块与大中枢派发
文章目录
- 块与大中枢派发
-
- 理解块这一概念
- 为常用的块创建typedef
- 用handler来降低代码分散度
- 用块引用所属对象时不要出现保留环
- 多用派发队列,少用同步锁
- 多使用GCD,少用performSelector方法
- 掌握GCD及操作队列的使用时机
- [通过Dispatch Group机制,根据系统资源状况来执行任务](#通过Dispatch Group机制,根据系统资源状况来执行任务)
- 使用dispatch_once来执行只需要运行一次的线程安全的代码
- 不要使用dispatch_get_current_queue
理解块这一概念
块这个概念和函数类似,只不过是直接定义在另一个函数内的,和定义他的那个函数共享同一个范围内的东西
objc
^{
//Block implementation
}
块的语法和函数指针类似:
objc
void (^someBlock)() = ^{};
returnType (^bolckName)(parameters);//语法格式
块的一个强大之处在与在他的范围内,所有变量都可以为其所捕获。这也就是说,范围内部的全部变量,在块内仍然可以使用:
objc
int add = 5;
int (^addBlock) (int a, int b) = ^(int a, int b){
return a + b + additional;
}
int add = addBlock (2, 5); //< add = 12
在默认情况下,为块所捕获的变量是不可以在块中被修改。不过如果在声明变量的时候加上一个__block这个修饰符,这样就可以在块里被修改了。
内联块的用法:
objc
NSArray *array = @[@0, @1, @2, @3, @4, @5];
block NSInteger count = 0;
[array enumerateObjectsUsingBlock:
^ (NSNumber *number, NSUInteger idx, BOOL *stop) (
if ([number compare: @2] == NSOrderedAscending) {
count++;
}
};
// count = 2
如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候,也会将其一并释放。这就引出了一个与块有关的重要问题。块本身可视为对象。实际上,在其他 Objective-C 对象所能响应的选择子中,有很多是块也可以响应的。而最重要之处则在于,块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量,以便平衡捕获时所执行的保留操作。
如果将块定义在 Objective-C类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self变量。块总能修改实例变量,所以在声明时无须加block。不过,如果通过读取或写入操作捕获了实例变量,那么也会自动把 self变量一并捕获了,因为实例变量是与self所指代的实例关联在一起的。
objc
@interface EOCClass
-(void) anInstanceMethod {
void (someBlock) () = ^(
_anInstanceVariable = @"Something";
NSLog (@ "anInstanceVariable = 80", _anInstanceVariable);
};
// ...
}
@end
如果保留self所指代的那个对象的同时也保留了块,那么这种情况通常会导致保留环。
内部结构
先认识一下我们的块的一个结构

invoke这个变量是指向块的实现代码,是一个函数指针,函数原型表示我们表示至少接受一个void类型的参数。这个参数代表块。
descriptor这个变量是指向结构体的一个指针,每个块内都包含此结构体,这个声明了块的一个整体大小,还声明了copy和dispose这两个辅助函数所对应的函数指针,
块会把他的所捕获的变量全部都拷贝一份,这些拷贝放在descriptor后面,捕获了多少个变量,就要占据多撒后的空间。执行块时,要从内存中 把这些变量读出来。
全局块,堆块,栈块
定义了块的时候,其所占的内存区域时分配在栈中的,下面这段代码其实就有风险:
objc
void (^block)();
if (a == 1) {
block = ^{
NSLog (@"Block A") ;
};
} else {
block = ^{
NSLog (@"Block B") ;
};
}
block () ;
定义在if及 else 语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块的内存覆写掉。于是,这两个块只能保证在对应的if或else 语句范围内有效。这样写出来的代码可以编译,但是运行起来时而正确,时而错误。若编译器未覆写待执行的块,则程序照常运行,若覆写,则程序崩溃。
为了解决这个问题,可以给块对象发生copy消息以拷贝。这样的话,就可以把块从栈复制到堆上,拷贝之后的块,可以在定义他的范围之外使用,一但复制到堆上,就成为带有引用计数的对象了,后续的复制操作都不会进行复制,而是递增引用计数。在ARC环境下会自动释放,在MRC环境下需要自己手动release。
objc
void (^block)();
if (a == 1) {
block = [^{
NSLog (@"Block A") ;
} copy];
} else {
block = [^{
NSLog (@"Block B") ;
} copy];
}
block () ;
除了这两个块,我们还有一类块叫做全句块。另外,全局块的拷贝操作是一个空操作,因为全局块不可能被系统回收。这种块实际上相当于单例。
要点
- 快是c,c++,OC的语法闭包
- 块可以接受参数,也可以返回值
- 块可以分配在栈或堆上,也可以是全局的,分配在栈上的块可以拷贝到堆上。
为常用的块创建typedef
objc
returnType (^bolckName)(parameters);//语法格式
这里我们定义块的名字不想别的类型变量,是放在类型之中。
所以我们会采用typedef
objc
typedef int (EOCSomeBlock) (BOOL flag, int value) ;
这样创建就更加方便了,也更加符合我门的一个创建的习惯
objc
EOCSomeBlock block = ^ (BOOL flag, int value) {
// Implementation
};
这样就和定义其他变量一样变量类型在左边,变量名在右边。
使用类型定义语句还有一个好处就是我们,重构类型的名字的时候会方便狠多:
objc
typedef void (^EOCCompletionHandler)
(NSData *data, NSTimeInterval duration, NSError*error) ;
要点
-
以 typedef 重新定义块类型,可令块变量用起来更加简单。
-
定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
-
不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应 typedef 中的块签名即可,无须改动其他 typedef。
用handler来降低代码分散度
假如把执行异步任务的方法做成同步的,那么在执行任务的时候,用户界面就会变得无法响应用户输入,所以我们一般采用block处理。
objc
typedef void (^EOCNetworkFetcherCompletionHandler) (NSData *data) ;
@interface EOCNetworkFetcher : NSObject
- (id) initwithURL: (NSURL*) url;
- (void) startWithCompletionHandler:
(EOCNetworkFetcherCompletionHandler) handler;
@end
- (void) fetchFooData {
NSURL *url = [[NSURL alloc] initWithString:
@ "http://www.example.com/foo.dat"];
EOCNetworkFetcher *fetcher =
[ [EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^ (NSData *data) {
_fetchedFooData = data;
};
}
这种API的设计很不错,他可以根据不同的情况获取不同的handle块来获取不同的内容,比方说我们有成功的返回和失败的返回,那么会进入不同的一个handler块。
要点
-
在创建对象时,可以使用内联的 handler 块将相关业务逻辑一并声明。
-
在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用 handler 块来实现,则可直接将块与相关对象放在一起。
-
设计 API 时如果用到了 handler 块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。
用块引用所属对象时不要出现保留环
这里直接看这段代码:
objc
@implementation EOCClass {
EOCNetworkFetcher * _networkFetcher;
NSData * fetchedData;
}
- (void) downloadData {
NSURL *url = [[NSURL alloc] initWithString:
@ "http://www.example.com/something.dat"];
_networkFetcher =
[ [EOCNetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^ (NSData *data) {
NSLog (@"Request URL 8@ finished", _networkFetcher.url) ;
_fetchedData = data;
}];
}
@end
这段代码看上去似乎没有什么问题,但是这段代码他捕获了实例变量,所以他必须捕获self变量。所以就会出现下面这种情况:

这里我们其实只用添加一条,也就是使用完EOCClass之后将NetworkFetcher直接释放掉就好了。也就是把他置为nil就可以了。
我们设置API的时候才用了这种handle的方式就会很经常性的出现保留环的一个问题。
如果 completionhandler 块所引用的对象最终又引用了这个块本身,那么就会出现保留环。比方说,我们修改一下前面那个例子,使调用API 的那段代码无须在执行期间保留指向网络数据获取器的引用,而是设定一套机制,令获取器对象自己设法保持存活。要想保持存活,获取器对象可以在启动任务时把自己加到全局的 collection 中(比如用 set 来实现这个 collection),待任务完成后,再移除。
这样的话我们就不需要找之前的方式样子自己手动释放newworkFetcher了。因为这里他会自己吧获取器对象给移除掉。
要点
- 如果块所捕获的对象直接或间接的保留了块本身,那么就要当心保留环问题
- 一定要找一个合适的机会解除保留环,
多用派发队列,少用同步锁
同步锁就是:
objc
@synchronized(self) {
}
这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。但是这样子频繁枷锁会降低很多效率。
大部分情况我们都是采用GCD来代替🔒来进行操作。
有一种简单而且高效的方法可以代替同步块或锁对象,那就是串行同步队列。:
objc
- (NSString*) someString {
__block NSString *localSomeString;
dispatch_sync (_syncQueue, ^{//这里使用的是一个串行对列
localSomeString =_someString;
});
return localSomeString;
}
这里我们看一下两段代码的一个差异:
objc
- (void) setSomeString: (NSString*) someString {
dispatch_sync (_syncQueue, ^{
_someString = someString;
});
}
- (void) setSomeString: (NSString*) someString {
dispatch_async (_syncQueue, ^{
_someString = someString;
});
}
这次只是把同步派发改成了异步派发,从调用者的角度来看,这个小改动可以提升设置方法的执行速度,而读取操作与写入操作依然会按顺序执行。但这么改有个坏处:如果你测一下程序性能,那么可能会发现这种写法比原来慢,因为执行异步派发时,需要拷贝块。若拷贝块所用的时间明显超过执行块所花的时间,则这种做法将比原来更慢。由于本书所举的这个例子很简单,所以改完之后很可能会变慢。然而,若是派发给队列的块要执行更为繁重的任务,那么仍然可以考虑这种备选方案。
如果采用的是并发队列的话,我们就需要采用栅栏函数来处理这部分代码:

在队列中,栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的块总是按顺序逐个来执行的。并发队列如果发现接下来要处理的块是个栅栏块(barrier block),那么就一直要等当前所有并发块都执行完毕,才会单独执行这个栅栏块。待栅栏块执行过后,再按正常方式继续向下处理。
这里我们看修改后的代码:
objc
- (NSString*) someString {
__block NSString *localSomeString;
dispatch_sync (_syncQueue, ^{//这里使用的是一个串行对列
localSomeString =_someString;
});
return localSomeString;
}
- (void) setSomeString: (NSString*) someString {
dispatch_barrier_async (_syncQueue, ^{
_someString = someString;
});
}

上面这张图就展示出了一个我们这里并发队列的样式,可以保证读写安全。
要点
-
派发队列可用来表述同步语义(synchronization semantic),这种做法要比使用@synchronized 块或 NSLock对象更简单。
-
将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。
-
使用同步队列及栅栏块,可以令同步行为更加高效。
多使用GCD,少用performSelector方法
这里我们看一下这段代码:
objc
SEL selector;
if (/* some condition */ ) {
selector = @selector (foo) ;
} else if (/* some other condition */ ) {
selector = @selector (bar) ;
} else {
selector = @selector (baz) ;
}
[object performSelector:selector];
你可能没料到会出现这种警告。要是早就料到了,那么你也许已经知道使用这些方法时为何要小心了。这条消息看上去可能比较奇怪,而且令人纳闷:为什么其中会提到内存泄漏问题呢?只不过是用 "performSelector:"调用了一个方法。原因在于,编译器并不知道将要调用的选择子是什么,因此,也就不了解其方法签名及返回值,甚至连是否有返回值都不清楚。而且,由于编译器不知道方法名,所以就没办法运用 ARC的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。然而这么做可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。
这里我们看另一段代码:
objc
SEL selector;
if (/* some condition */ ) {
selector = (selector (newObject) ;
} else if (/* some other condition */ ) {
selector = @selector (copy) ;
} else {
selector = @selector (someProperty) ;
}
id ret = [object performSelector: selector];
这里如果调用的是前两个选择子中的任意一个,我们都会发现这里的ret会由这单代码来释放,但是如果是第三个选择子则意味着我们不用释放,这样子就很容易出现一个内存泄漏的问题。这里还有不好的地方在于,返回值只能是void和对象类型,不能返回整形和浮点型的内容。
尽管有其他方法来处理这部分内容,但是还是局限很多:
objc
- (id) performSelector: (SEL) selector
withObject: (id) object
- (id) performSelector: (SEL) selector
withObject: (id) objectA withObject: (id) objectB
但是还是只能传输我们的参数类型还都是id类型,依旧不可以传输我们的一个整数类型。
要点
- performSelector 系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么,因而 ARC编译器也就无法插人适当的内存管理方法。
- performSelector 系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到限制。
- 如果想把任务放在另一个线程上执行,那么最好不要用 performSelector 系列方法,而是应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现。
掌握GCD及操作队列的使用时机
我们得先认识一下两者的一个区别:
在两者的诸多差别中,首先要注意:GCD是纯C的API,而操作队列则是 Objective-C的对象。在GCD 中,任务用块来表示,而块是个轻量级数据结构(参见第37条)。与之相反,"操作"(operation)则是个更为重量级的Objective-C对象。虽说如此,但GCD并不总是最佳方案。有时候采用对象所带来的开销微乎其微,使用完整对象所带来的好处反而大大超过其缺点。
接下来简单介绍一下这个操作队列的内容,他有一些优点:
- 取消某一个操作。如果使用操作队列,那么想取消操作是很容易的。在运行任务之前,可以在NSOperation上面调用cancel方法,这个方法会设置对象内的标志位,用来表明该任务不用执行。但是如果安排到GCD中,就无法取消任务了。
- 指定操作之间的依赖关系,开发者能够制定操作之间的一个依赖体系,使特定的操作必须在另外一个操作顺利完成之后才可以执行
- 通过简直观测机制来监控NSOperation对象的属性。NSOperation中很多属性哦度可以KVO来进行监听
- 指定操作之间的一个优先级。操作之间的优先级表示这个操作和其他操作之间的优先级关系
- 可以冲用NSoperation对象。系统内置了一些NSOperation的子类共开发者来调用,也可以自己创建一些子类。
要点
- 解决多线程与任务管理问题是,派发队列并非唯一方案
- 操作队列提供了一套高层的OCAPI,能实现GCD所具备的绝大部分功能而且还能完成一些更加复杂的操作,哪些操作如果该用GCD来实现,则需要另外编写代码。
通过Dispatch Group机制,根据系统资源状况来执行任务
Dispatch Group这个是GCD的一个特性,能够将任务分组。调用者可以等待这组任务执行完毕之后,也可以提供回调函数之后继续往下执行,这组任务完成之后,调用者会得到通知。
objc
dispatch_group_t dispatch_group_create();
我们有下面这两个函数来指定任务所属的一个group
objc
dispatch_group_enter(<#dispatch_group_t _Nonnull group#>)
dispatch_group_leave(<#dispatch_group_t _Nonnull group#>)
前者能够给任务添加到对应的组中。后者是移除任务,每一个enter都要有对应的一个leave操作。
下面这个函数用于等待group执行结束:
objc
dispatch_group_wait(<#dispatch_group_t _Nonnull group#>, <#dispatch_time_t timeout#>)
一个表示要等待的group,另一个则是代表等待时间的timeout值,后面表示如果执行完毕之后应该阻塞多久,这里也可以采用DISPATCH_TIME_FOREVER这个表示知道dispatch_group执行完,不会超时。
除了上面那个函数之外我们还可以看一下下面这个函数:
objc
dispatch_group_notify(<#dispatch_group_t _Nonnull group#>, <#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
这里是开发者想这个函数传入块,块就会在group执行完之后在特定一个线程上执行。假如当前线程不应阻塞,而开发者有希望在哪些任务完成之后得到通知。就可以采用这个函数来处理
如果想让数组中的每个对象都执行某项任务,并且想等待所有任务执行完毕,那么就可以使用GCD这个特性来实现。
objc
dispatch_queue_t queue =
dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) ;
dispatch_group_t dispatchGroup = dispatch_group_create () ;
for (id object in collection) {
dispatch_group_async (dispatchGroup, queue,^{
[object performTask]; });
dispatch_group_wait (dispatchGroup, DISPATCH_TIME_FOREVER);
}
// Continue processing after completing tasks
这个例子是所有任务都派发到同一队列中。但十几岁未必一定要这么做,也可以把任务放在优先级高的线程上去执行,同时仍然吧所有任务都归入同一个group中:

除了像这样把任务给并发队列外,也可以提交到各个穿行队列中,并且用group来跟踪其执行情况。但是这样的话group存在的意义就不打了,这样和提交完所有任务后一个块就可以了。

所以有些时候我们没必要使用dispatch group。有些时候采用单个队列搭配标准的异步并发,也可以实现同样的效果。
通过 dispatch group 所提供的这种简便方式,既可以并发执行一系列给定的任务,又能在全部任务结束时得到通知。由于GCD有并发队列机制,所以能够根据可用的系统资源状况来并发执行任务。而开发者则可以专注干业务罗辑代码,无须再为了外理并发任务而编写复杂的调度器。
在前面的范例中完,我们遍历collection的时候我们也可以采用另外一个gcd来实现。
objc
dispatch_apply(<#size_t iterations#>, <#dispatch_queue_t _Nullable queue#>, <#^(size_t iteration)block#>)
这个函数会将块反复执行一定的次数,每次穿给块的参数值会递增,从0开始一直到iterations - 1。
objc
dispatch_queue_t queue =
dispatch_queue_create ("com. effectiveobjectivec. queue", NULL) ;
dispatch_apply(10, queue, ^ (size_t i) {
// Perform task
}) ;
apply所用的队列可以是并发队列。如果是并发队列的话,那么系统就要根据资源状况来处理这些块了,这与group那段代码就一样了。
这里要注意apply会持续堵塞,知道所有任务都执行完毕位置,由此可见,假如把块派给了当前队列,那么就讲导致思所。如果想在后台执行任务就应该使用dispatch group。
dispatch_apply
:
dispatch_apply
是一个同步的函数,用于并行执行多个任务。它会等待所有的任务都执行完毕才会继续往下执行代码。- 它会阻塞当前线程,直到所有的任务执行完毕。如果你将任务分配到当前线程(或者是主线程)上,就会导致阻塞,使得程序无法继续执行后续的代码,进而影响性能或造成死锁。
dispatch_group
:
dispatch_group
则是一种异步方式,适合用于并行执行多个任务并在所有任务完成后统一处理(比如通知或回调)。它不会像dispatch_apply
那样阻塞当前线程,可以避免卡住线程的问题。- 使用
dispatch_group
,你可以将多个任务添加到组中,等所有任务完成后再执行后续操作,而这个过程是异步的,不会卡住线程。(引自chatgpt)总结:
- 如果需要并行执行任务并等待结果,但不希望阻塞当前线程(比如主线程),使用
dispatch_group
比dispatch_apply
更加合适。dispatch_apply
阻塞当前线程,可能在当前队列上执行时造成死锁,因此应避免在主队列上调用它。
要点
- 一系列任务可归入一个 dispatch group 之中。开发者可以在这组任务执行完毕时获得通知。
- 通过 dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD 会根据系统资源状况来调度这些并发执行的任务。开发者若自己来实现此功能,则需编写大量代码。
使用dispatch_once来执行只需要运行一次的线程安全的代码
单例模式这里就会用到一个全类共用的一个单例实例:
objc
+ (id) sharedInstance i
static EOCClass *sharedInstance = nil;
@synchronized (self) {
if (!sharedInstance) {
sharedInstance = [[self alloc] initl;
}
}
return sharedInstance;
}
然后我们可以用GCD来实现我们的一个单例模式:
objc
+ (id) sharedInstance {
static EOCClass *sharedInstance = nil;
static icdispatch_once_t onceToken;
dispatch_once (&onceToken, ^(
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
使用 dispatch_once 可以简化代码并且彻底保证线程安全,开发者根本无须担心加锁或同步。所有问题都由GCD 在底层处理。由于每次调用时都必须使用完全相同的标记,所以标记要声明成 static。把该变量定义在static 作用域中,可以保证编译器在每次执行sharedlnstance 方法时都会复用这个变量,而不会创建新变量。
此外,dispatch_once 更高效。它没有使用重量级的同步机制,若是那样做的话,每次运行代码前都要获取锁,相反,此函数采用"原子访问"(atomic access)来查询标记,以判断其所对应的代码原来是否已经执行过。笔者在自己装有64位 Mac OS X 10.8.2系统的电脑上简单测试了性能,分别用 @synchronized 方式及 dispatch L_once 万式来买现 sharedInstance方法,结果显示,后者的速度几乎是前者的两倍。
要点
- 经常需要编写"只需执行一次的线程安全代码"(thread-safe single-code execution)。通过GCD 所提供的 dispatch_once函数,很容易就能实现此功能。
- 标记应该声明在 static 或global 作用域中,这样的话,在把只需执行一次的块传给dispatch_once函数时,传进去的标记也是相同的。
不要使用dispatch_get_current_queue
使用GCD的时候,经常需要判断当前代码在那个队列上执行,向多个队列派发任务的时候,有些时候进场判断代码在那个线程执行。
objc
dispatch_get_current_queue()
该函数有种典型的错误用法(antipattern,"反模式"),就是用它检测当前队列是不是某个特定的队列,试图以此来避免执行同步派发时可能遭遇的死锁问题。
objc
- (NSString*) someString {
__block NSString *localSomeString;
dispatch_sync (_syncQueue, ^{
localSomeString =_someString;
});
return localSomeString;
}
- (void) setSomeString: (NSString*) someString {
dispatch_async (_syncQueue, ^(
_someString = someString;
}) ;
}
这里其实存在一个死锁的问题,如果是同步操作所针对的队列就会出现一个死锁的问题(在本例中是_syncQueue)_
_这种写法的问题在于,获取方法(getter)可能会死锁,假如调用获取方法的队列恰好是同步操作所针对的队列(本例中是_syncQueue),那么 dispatch_sync 就一直不会返回,直到块执行完毕为止。可是,应该执行块的那个目标队列却是当前队列,而当前队列的dispatch_sync 又一直阻塞着,它在等待目标队列把这个块执行完,这样一来,块就永远没机会执行了。像someString这种方法,就是"不可重入的"。
知道前面那个方法后,我们可能会出现下面的代码:
objc
- (NString*) someString i
__block NSString *localSomeString;
dispatch_block_t accessorBlock = ^{
localSomeString =_someString;
} ;
if (dispatch_get_current_queue() == _syncQueue) {
accessorBlock () ;
} else {
dispatch_sync (_syncQueue, accessorBlock);
}
return localSomeString;
}
但是仍然会有死锁的情况。
这里假设有两个穿行派发队列:
objc
dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_sync(queueA, ^{
// Deadlock
});
});
});
这段代码执行到最内层的派发操作的时候,总会死锁,因为此操作是针对A队列的,所以要等到最外的dispatch_sync执行完才行,但是这里外部的有需要等待内部的sync执行完之后才能执行,所以就死锁。现在按照之前的做法用这个函数来做检测:
objc
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{ /* ... */ };
if (dispatch_get_current_queue() == queueA) {
block();
} else {
dispatch_sync(queueA, block);
}
});
});
但是这样做仍然是死锁,因为他返回的是queueB永远会进入第二个,然后还是死锁。
在这种情况下,正确做法是:不要把存取方法做成可重人的,而是应该确保同步操作所用的队列绝不会访问属性,也就是绝对不会调用 someString 方法。这种队列只应该用来同步属性。由于派发队列是一种极为轻量的机制,所以,为了确保每项属性都有专用的同步队列,我们不妨创建多个队列。
下面展示一下一个队列之间的一个层级体系,在某一条队列中的块,会在他的上级队列中执行

由于队列间有层级关系,所以"检查当前队列是否为执行同步派发所用的队列"这种办法,并不总是奏效。比方说,排在队列C里的块,会认为当前队列就是队列C,而开发者可能会据此认定:在队列A上能够安全地执行同步派发操作。但实际上,这么做依然会像前面那样导致死锁。
有的API 可令开发者指定运行回调块时所用的队列,但实际上却会把回调块安排在内部的串行同步队列上,而内部队列的目标队列又是开发者所提供的那个队列,在此情况下,也许就要出现刚才说的那种问题了。使用这种 API 的开发者可能误以为:在回调块里调用dispatch_get_current_queue所返回的"当前队列",总是其调用API 时指定的那个。但实际上dispatch_get_current返回的却是API 内部的那个同步队列。
要解决这个问题,最好的办法就是通过GCD 所提供的功能来设定"队列特有数据"(queue-specific data),此功能可以把任意数据以键值对的形式关联到队列里。最重要之处在于,假如根据指定的键获取不到关联数据,那么系统就会沿着层级体系向上查找,直至找到数据或到达根队列止。笔者这么说,大家也许还不太明白其用法,所以来看下面这个例子:
objc
dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);
dispatch_set_target_queue(queueB, queueA);
static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA,
&kQueueSpecific,
(void*)queueSpecificValue,
(dispatch_function_t)CFRelease);
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{
NSLog(@"No deadlock!");
};
CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
if (retrievedValue) {
block();
} else {
dispatch_sync(queueA, block);
}
});
队列A的目标队列是默认优先级的全局并发队列,队列B德目标队列是队列A,而队列A的目标队列仍然是默认优先级的全局并发队列,然后给A队列设置队列特定值:
objc
void dispatch_queue_set_specific(dispatch_queue_t queue,
const void *key,
void *context,
dispatch_function_t destructor);
参数说明:
queue
:
要为其设置特定值的队列。可以是任何dispatch_queue_t
类型的队列。key
:
一个指向常量void*
的指针,它是用于标识队列中特定值的键。通常是一个唯一的标识符,用来关联队列和它所包含的特定值。多个值可以使用不同的键来存储。context
:
一个指向void*
的指针,表示你想要存储在队列中的特定值。可以是任何类型的数据,它将在指定的队列中与key
相关联。destructor
:
这是一个函数指针,当队列被销毁时,会调用这个函数来清理context
(特定值)。该函数的签名是dispatch_function_t
类型,通常是用于释放context
或做其他清理工作。
key和context是键与值。这两个都是不透明的void指针,这里注意函数是通过函数的指针值来比对建的,而不是按照他的内容。所以,队列特定数据和NSDictonary对象不同,后者是比较对象等同性,这里的值更类似于关联引用。值也是不透明的void指针,于是可以在其中存放任意数据。然而必须管理这个对象的内存,这使得在ARC环境xAI很难用OC对象作为值,范例中使用Core Foundation的值,因为ARC不会处理这个Core Foundation的值,所以它适合充当对应的队列特定数据。
最有一个函数是作为析构函数的,用来回收context的内容,有新的值和建关联的时候,原来的值就会被移除,这时候就会调用这个析构函数,所以这里采用CFRelease。
于是,"队列特定数据"所提供的这套简单易用的机制,就避免了使用 dispatch_getcurrent_queue 时经常遭遇的一个陷阱。此外,调试程序时也许会经常用到 dispatch_getcurrent_queue。在此情况下,可以放心使用这个已经废弃的方法,只是别把它编译到发行版的程序里就行。如果对"访问当前队列"这项操作有特需求,而现有函数又无法满足,那么最好还是联系苹果公司,请求其加入此功能。
要点
- dispatch_get_current_queue 函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试之用。
- 由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述"当前队列"这一概念。
- dispatch_get_current_queue 函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用"队列特定数据"来解决。