今天最终攻克了多线程同一时候訪问数据库时,报数据库锁定的问题。错误信息是:
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就会完毕线程的创建,分配与回收