Effective Objective 2.0 读书笔记 之 GCD

      Grand Central Dispatch (GCD) 是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级中实现。开发者只需要定义想执行的任务并追加到适当的Disptach Queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可执行任务,这样就比以前的线程更有效率。
      GCD用我们难以置信的非常简洁的记述方法,实现了极为复杂繁琐的多线程编程。下面列出几点关于GCD使用的高效方法。

一:使用Dispatch_once来执行只需运行一次的线程安全代码(单例)

      单例模式大家都不陌生,常见的实现方式为:在类中编写名为sharedInstance的类方法,该方法只会返回全类共用的单例实例,而不会每次调用时都创建新的实例。例如:

1
2
3
4
5
6
7
8
9
+(void)sharedInstance{
static DemoClass *sharedInstance = nil;
@synchronized(self){
if(!sharedInstance){
sharedInstance = [[self alloc]init];
}
}
return sharedInstance;
}

      这种单例模式在线程安全的问题下引起激烈争论。不过,GCD引入了一项特性dispatch_once,此函数采用“原子访问(atomic access)来查询标记”,能使单例实现起来更为容易,而且完全是线程安全的。并且dispatch_once更高效(速度是synchronized的两倍),没必要使用synchronized这种重量级的同步机制。改写后:

1
2
3
4
5
6
7
8
9
+(void)sharedInstance{
static DemoClass *sharedInstance = nil;
//对于只需要执行一次的单例来说每次调用函数时传入的标记必须完全相同,通常声明在static或者global作用域
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc]init];
});
return sharedInstance;
}

二:不要使用dispatch_get_current_queue

      该函数有个典型的错误用法,就是用它检测当前队列是不是某个特定的队列,视图以此来避免执行同步派发时可能遭遇的死锁问题。如下面这个代码,用队列来保证对实例变量的访问操作是同步的:

1
2
3
4
5
6
7
- (NSString)someString{
__block NSString *localSomeString;
dispatch_sync(_syncQueue,^{
localSomeString = _someString;
});
return localSomeString;
}

      这种写法的问题在于可能会死锁,假如调用获取方法的队列恰好是同步操作锁针对的队列_syncQueue,那么dispatch_syncdispatch_sync就会一直不返回,直到块执行完毕,可应该执行块的目标队列确实当前队列,当前队列又在阻塞着。someString方法就变成“不可重入的”。
利用dispatch_get_current_queue可让方法“可重入”:
1
2
3
4
5
6
7
8
9
10
11
12
- (NSString)someString{
__block NSString *localSomeString;
dispatch_block_t block = ^{
localSomeString = _someString;
};
if(dispatch_get_current_queue() == _syncQueue){
block();
}else{
dispatch_sync(_syncQueue,block);
}
return localSomeString;
}

      以上写法确实能解决一些简单情况,但仍然有死锁的危险。考虑下面这两个串行队列:
1
2
3
4
5
6
7
8
9
dispatch_queue_t queueA = dispatch_queue_create("com.test.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.test.queueB", NULL);
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_sync(queueA, ^{
//这里会死锁
});
});
});

      若使用dispatch_get_current_queue方法解决:
1
2
3
4
5
6
7
8
9
10
11
12
dispatch_queue_t queueA = dispatch_queue_create("com.test.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.test.queueB", NULL);
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{};
if(dispatch_get_current_queue() == queueA){
block();
}else{
dispatch_sync(queueA, block);//命中,但是最外层还是queueA,仍然造成死锁
}
});
});

      然而这样仍然造成死锁,因为dispatch_get_current_queue返回的是当前队列,还是会执行queueA,还是死锁。这种例子其实并非不常见,比如有的API可令开发者指定运行回调块时所用的队列(队列A),但实际上却会把回调块安排在内部的串行同步队列上(队列B),而内部的目标队列又是开发者所提供的那个队列,这种情况下就出现上面所说的问题了。开发者可能会误认为在回调块中调用dispatch_get_current_queue返回的队列是调用API时指定的那个队列(队列A),但是实际上返回的却是内部的那个同步队列(队列B,因为前面说了回调被安排在内部串行同步队列上了,而开发者并不知道)。解决这个问题可以通过GCD所提供的功能来设定“队列特有数据”,此数据以键值对的形式关联到队列里。关键在于,假如根据指定的建获取不到关联数据,那么系统就会沿着层级体系向上查找,直至找到数据或到达根队列为止。
      例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
dispatch_queue_t queueA = dispatch_queue_create("com.test.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.test.queueB", NULL);
//将队列B的目标队列设为队列A,而A的目标队列仍是默认优先级的全局并发队列
dispatch_set_target_queue(queueB, queueA);
static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");
//在队列A上设置“队列特定值”,队列参数后两个参数分别是键值,都是不透明void指针
//必须管理该对象的内存,所以这里适合使用CoreFoundation字符作为值,
//并用最后一个参数析构函数CFRelease来清理旧值。也可以用自定义函数,在其中调用CFRelease清理旧值。
dispatch_queue_set_specific(queueA,
&kQueueSpecific,
(void*)queueSpecificValue,
(dispatch_function_t)CFRelease);
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{
NSLog(@"没有死锁哦~");
};
CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
if(retrievedValue){
block();
}else{
dispatch_sync(queueA, block);
}
});

      note:函数是按指针值来比较键的,不是按照内容。所以“队列特定数据”与NSDictionary对象不同,后者是比较键的“对象等同性”。“队列特定数据”更新是“关联引用”,关联引用简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void *AlertAssociateKey = @"AlertAssociateKey";
- (void)testAssociated {
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:@"test"
message:@"associated"
delegate:self
cancelButtonTitle:@"cancel"
otherButtonTitles:@"ok", nil];
void (^block)(NSInteger) = ^(NSInteger index){
if(index == 0){
NSLog(@"cancel");
}else{
NSLog(@"ok");
}
};
objc_setAssociatedObject(alert, AlertAssociateKey, block, OBJC_ASSOCIATION_COPY);
[alert show];
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, AlertAssociateKey);
block(buttonIndex);
}

      “队列特定数据”所提供的这套简单易用的机制,避免了使用dispatch_get_current_queue经常遭遇的一个陷阱。