• 使用FMDB多线程訪问数据库,及database is locked的问题


    今天最终攻克了多线程同一时候訪问数据库时,报数据库锁定的问题。错误信息是:

    Unknown error finalizing or resetting statement (5: database is locked)

    最后通过FMDatabaseQueue攻克了这个问题。本文总结一下:

    FMDatabase不能多线程使用同一个实例

    多线程訪问数据库,不能使用同一个FMDatabase的实例,否则会发生异常。假设线程使用单独的FMDatabase实例是同意的,可是相同有可能发生database is locked的问题。

    这是因为多线程对sqlite的竞争引起的

    我的app一開始就是多线程使用单独的FMDatabase实例訪问数据库,尽管没有引起crash。可是还是出现了database is locked问题,造成非常多数据没有如预期写入数据库

    使用FMDatabaseQueue,问题依然

    后来上FMDB的官网看了文档,确认用FMDatabaseQueue能够解决问题,API也比較简单:

    NSString *dbFilePath = [PathResolver databaseFilePath];
    queue = [FMDatabaseQueue databaseQueueWithPath:dbFilePath];
    [queue inDatabase:^(FMDatabase *db){
        // access db
    }];
    可是实际測试了一下,还是database is locked

    读了一下相关的源代码。FMDatabaseQueue解决问题的思路是:创建一个队列。然后将放入队列的block顺序运行,这样避免了多线程同一时候訪问数据库

    而我的代码是多线程各创建FMDatabaseQueue的实例,所以事实上有多个队列,因此还是存在数据库竞争的问题,和用FMDatabase时是一样的

    共享同一个FMDatabaseQueue实例

    于是接下来我让每一个线程使用同一个Queue实例。问题就顺利攻克了

    实现的方式,一開始我想给FMDatabase添加一个单例方法。可是这样以后升级FMDB会比較麻烦。所以最后我是创建了一个Helper类

    @implementation LosDatabaseHelper
    
    {
        FMDatabaseQueue* queue;
    }
    
    -(id) init
    {
        self = [super init];
        if(self){
            NSString *dbFilePath = [PathResolver databaseFilePath];
            queue = [FMDatabaseQueue databaseQueueWithPath:dbFilePath];
        }
        return self;
    }
    
    +(LosDatabaseHelper*) sharedInstance
    {
        static dispatch_once_t pred = 0;
        __strong static id _sharedObject = nil;
        dispatch_once(&pred, ^{
            _sharedObject = [[self alloc] init];
        });
        return _sharedObject;
    }
    
    -(void) inDatabase:(void(^)(FMDatabase*))block
    {
        [queue inDatabase:^(FMDatabase *db){
            block(db);
        }];
    }
    
    @end

    系统中其它的类。使用这个Helper类的单例,这样保证了全局仅仅有唯一的FMDatabaseQueue实例。

    注意,由于Helper内部持有的是FMDatabaseQueue,所以能够这么做。假设包装的是FMDatabase类。就绝对会有问题。由于FMDatabase实例不能在多线程环境共享

    使用FMDatabaseQueue之后。管理db

    原本使用FMDatabase类,须要手工调用db的open和close方法

    可是用FMDatabaseQueue,不须要调用open。由于查看代码发现,Queue已经open了。至于要不要close,我也不确定,由于官方的sample code没有调用close。实际应用中,我也没有调用。好像没有问题。假设须要close的话,我想能够在Helper类的公共方法里添加调用close queue就能够了。以下是close的源代码:

    - (void)close {
        FMDBRetain(self);
        dispatch_sync(_queue, ^() { 
            [_db close];
            FMDBRelease(_db);
            _db = 0x00;
        });
        FMDBRelease(self);
    }

    所以。使用Queue,是不须要自己打开和关闭db的。可是假设使用了FMResultSet,rs倒是须要关闭,否则会报warning:

    if ([db hasOpenResultSets]) {
        NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]");

    为了不看到warning,我都在block里调用了[rs close]

    刷新数据库文件路径

    详细到我们的应用,另一个特殊问题须要考虑。

    由于我们的APP能够切换账户,而账户的db文件是独立的。所以当用户又一次登录的时候,须要刷新一下Helper的queue

    +(void) refreshDatabaseFile
    {
        LosDatabaseHelper *instance = [self sharedInstance];
        [instance doRefresh];
    }
    
    -(void) doRefresh
    {
        NSString *dbFilePath = [PathResolver databaseFilePath];
        queue = [FMDatabaseQueue databaseQueueWithPath:dbFilePath];
    }

    假设不这么做,因为Helper是单例,那么切换账户以后。用户B訪问的还是用户A的数据库。刷新的调用。一般放在登录之后。进入主页面之前就能够了

    队列和线程

    在debug过程中,顺便看到一个现象。尽管多个block都是放到同一个队列里。可是事实上是跑在不同的thread里

    不要混淆队列和线程的概念。使用GCD时。开发人员关注的是把block放到队列中,可是同一个队列事实上能够相应多个thread,为block分配thread,是GCD框架负责的,开发人员不须要关注。

    仅仅要把操作放到合适的队列里,GCD就会完毕线程的创建,分配与回收


  • 相关阅读:
    Train Problem(栈的应用)
    Code obfuscatio (翻译!)
    Milking Cows
    Sorting a Three-Valued Sequence(三值排序)
    Asphalting Roads(翻译!)
    FatMouse' Trade
    Fibonacci Again
    Yogurt factory
    经济节约
    Lucky Conversion(找规律)
  • 原文地址:https://www.cnblogs.com/clnchanpin/p/7204209.html
Copyright © 2020-2023  润新知