FMDatabaseQueue 如何保证线程安全

这篇文章原来在用 Github Pages 搭建的博客上,现在决定重新用回博客园,所以把文章搬回来。

FMDB 是 OC 针对 sqlite 的封装。在其文档的线程安全部分这样讲:同时从多个线程使用同一个FMDatabase的实例是一个糟糕的想法。在单个线程中使用FMDatabase没有问题,但是不要在线程间共享一个FMDatabase的对象。如果你不听劝阻,那么坏的事情将接踵而至,比如应用崩溃或者异常,也有可能有陨石从天上掉下来砸向你的 Mac Pro(还好买不起垃圾桶)。这相当糟糕。

Using a single instance of FMDatabase from multiple threads at once is a bad idea. It has always been OK to make a FMDatabase object per thread. Just don't share a single instance across threads, and definitely not across multiple threads at the same time. Bad things will eventually happen and you'll eventually get something to crash, or maybe get an exception, or maybe meteorites will fall out of the sky and hit your Mac Pro. This would suck.

同时,文档也给出了线程安全的方法,即使用FMDatabaseQueue。这篇文章就来分析 FMDatabaseQueue是如何做到线程安全的。

从初始化说起

+ (instancetype)databaseQueueWithPath:(NSString*)aPath;
+ (instancetype)databaseQueueWithPath:(NSString*)aPath flags:(int)openFlags;
- (instancetype)initWithPath:(NSString*)aPath;
- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags;

FMDatabaseQueue这四个初始化方法,其最终都会调用到initWithPath:flags:方法。其实现如下:

- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags {

    self = [super init];

    if (self != nil) {

        _db = [[[self class] databaseClass] databaseWithPath:aPath];
FMDBRetain(_db); #if SQLITE_VERSION_NUMBER >= 3005000
BOOL success = [_db openWithFlags:openFlags];
#else
BOOL success = [_db open];
#endif
if (!success) {
NSLog(@"Could not create database queue for path %@", aPath);
FMDBRelease(self);
return 0x00;
} _path = FMDBReturnRetained(aPath); _queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
_openFlags = openFlags;
} return self;
}

首先为实例化FMDatabase的一个实例_db,然后打开数据库。生成一个串行队列,然后调用dispatch_queue_set_specific为生成的 queue 设置关联的上下文数据。

这里需要注意两点,一是_db_path都是FMDatabaseQueue的数据成员,二是为生成的串行队列关联的上下文数据是self,即FMDatabaseQueue本身。

inDatabase:

- (void)inDatabase:(void (^)(FMDatabase *db))block {
/* Get the currently executing queue (which should probably be nil, but in theory could be another DB queue
* and then check it against self to make sure we're not about to deadlock. */
FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock"); FMDBRetain(self); dispatch_sync(_queue, ^() { FMDatabase *db = [self database];
block(db); if ([db hasOpenResultSets]) {
NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]"); #if defined(DEBUG) && DEBUG
NSSet *openSetCopy = FMDBReturnAutoreleased([[db valueForKey:@"_openResultSets"] copy]);
for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
NSLog(@"query: '%@'", [rs query]);
}
#endif
}
}); FMDBRelease(self);
}

开始的两行稍后讨论,我们往下看。

使用dispatch_sync同步派发到_queue,然后通过[self database]获取FMDatabase对象来执行对数据库的操作。自己可以看一下[self database]方法,实际上还是获取了初始化方法中的_db对象。这样就确保了唯一的FMDatabase对象在一个串行队列_queue中执行,而且是 sync

FMDatabaseQueue中其他的方法,思路与此一致,不再讨论。

处理潜在的死锁问题

这里有一个问题,如果调用dispatch_sync的队列与其派发的队列是同一个队列,而且都是串行队列。那么会发生什么?没错,死锁

我们在初始化函数中把FMDatabaseQueue的成员_queue初始化为了一个串行队列,那么如果调用inDatabase方法的队列跟_queue是同一个队列,就会造成死锁。开始的两行就是来处理这个问题。

当然,这发生的几率很小,一是初始化方法中dispatch_queue_create的第一个参数(用于指定队列的标签)是这样的[[NSString stringWithFormat:@"fmdb.%@", self] UTF8String],二是dispatch_queue_set_specific的key是这样的static const void * const kDispatchQueueSpecificKey = &kDispatchQueueSpecificKey;。如果你不故意要撞车,这个问题发生的概率要比中五百万的概率低得多。

上一篇:linux grep练习


下一篇:[bzoj1227] [SDOI2009]虔诚的墓主人