本文摘录自《Objective-C高级编程》一书,附加一些自己的理解,作为对GCD的总结。
此篇主要包含以下几个方面:
-
dispatch_suspend / dispatch_resume
-
dispatch_once
-
Dispatch Semaphore
- dispatch_semaphore_t
- dispatch_semaphore_create()
- dispatch_semaphore_wait()
- dispatch_semaphore_signal()
dispatch_suspend / dispatch_resume
当追加大量处理到Dispatch Queue时,在追加处理的过程中,有时希望不执行已追加的处理。例如演算结果被Block截获时,一些处理会对这个演算结果造成影响。
在这种情况下,只要挂起Dispatch Queue即可。当可以执行时再恢复。
dispatch_suspend
函数挂起指定的Dispatch Queue。
dispatch_suspend(queue);
dispatch_resume
函数恢复指定的Dispatch Queue。
dispatch_resume(queue);
这些函数对已经执行的处理没有影响。挂起后,追加到Dispatch Queue中但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。
注:
dispatch_suspend
函数和dispatch_resume
函数都可以用在Dispatch Source,而挂起和恢复的就是dispatch_source_set_event_handler
函数的回调。
dispatch_once
dispatch_once
函数是保证在应用程序执行中只执行一次指定处理的API。下面这种经常出现的用来进行初始化的源代码可通过dispatch_once
函数简化。
static int initialized = NO;
if (initialized == NO) {
/*
* 初始化
*/
initialized = YES;
}
如果使用dispatch_once
函数,则源代码写为:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/*
* 初始化
*/
});
源代码看起来没有太大的变化。但是通过dispatch_once
函数,该源代码即使在多线程环境下执行,也可保证百分之百安全。
之前的源代码在大多数情况下也是安全的。但是在多核CPU中,在正在更新表示是否初始化的标志变量时读取,就有可能多次执行初始化处理。而用dispatch_once
函数初始化就不必担心这样的问题。这就是所说的单例模式,在生成单例对象时使用。
Dispatch Semaphore
当并行执行的处理更新数据时即多个线程同时访问同一数据,会产生数据不一致的情况,有时应用程序还会异常结束。虽然使用Serial Dispatch Queue 和dispatch_barier_async
函数可避免这类问题,但有必要进行更细粒度的排他控制。
我们来思考一下这种情况:使用两个线程去访问同一个数据,以下代码countNumber最终结果是多少。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < 10000; i++) {
self.countNumber = self.countNumber + 1;
NSLog(@"%d",self.countNumber);
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < 10000; i++) {
self.countNumber = self.countNumber + 1;
NSLog(@"%d",self.countNumber);
}
});
最终结果并不是20000,这就是典型的线程安全问题。
因为该源代码使用Global Dispatch Queue 更新countNumber属性,所以执行后数据有很高概率并不是实时有效的,程序很可能异常结束。此时应使用Dispatch Semaphore。
Dispatch Semaphore本来使用的是更细粒度的对象,不过本书还是使用该源代码对Dispatch Semaphore进行说明。
Dispatch Semaphore是持有计数的信号,该计数是多线程编程中的计数类型信号。所谓信号,类似于过马路时常用的手旗。可以通过时举起手旗,不可通过时放下手旗。而在Dispatch Semaphore中,使用计数来实现该功能。计数为0时等待,计数为1或大于1时代码可通过。
下面介绍一下使用方法。通过dispatch_semaphore_create
函数生成Dispatch Semaphore。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
参数表示计数的初始值。本例将计数值初始化为“1”。
dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait
函数等待Dispatch Semaphore的计数值达到大于或等于1。当计数值大于等于1,或者在待机中计数值大于等于1时,对该计数进行减法并从dispatch_semaphore_wait
函数返回。第二个参数与dispatch_group_wait
函数等相同,由dispatch_time_t
类型值指定等待时间。该例的参数意味着永久等待。另外,dispatch_ semaphore_wait
函数的返回值也与dispatch_group_wait
函数相同。可像以下源代码这样,通过返回值进行分支处理。
dispatch_semaphore_t sem = dispatch_semaphore_create(1);
long result = dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
if (result == 0) {
/*
* 由于 Dispatch Semaphore 的计数值达到大于等于1
* 或者在待机中的指定时间内 Dispatch Semaphore 的计数值达到大于等于1
* 所以 Dispatch Semaphore 的计数值减去1。
*
* 可执行需要进行排他控制的处理
*/
}
else {
/*
* 由于 Dispatch Semaphore 的计数值为0
* 因此在达到指定时间为止待机
*/
}
dispatch_semaphore_wait
函数返回0时,可安全地执行需要进行排他控制的处理。该处理结束时通过dispatch _semaphore_signal
函数将Dispatch Semaphore的计数值加1。
我们在前面的源代码中实际使用Dispatch Semaphore看看。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
* 生成 Dispatch Semaphore。
*
* Dispatch Semaphore 的计数初始值设定为“1”。
*
* 保证可访问 countNumber 属性的线程
* 同时只能有一个
*/
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_async(queue, ^{
for (int i = 0; i < 10000; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
* 执行过 dispatch_semaphore_wait 后计数为 0 ,其它线程需要等待 Dispatch Semaphore ,
*
* 一直等待,直到 Dispatch Semaphore 的计数值达到大于等于 1 。
*
* 由于可访问 countNumber 属性的线程只有一个
* 因此可以安全的进行更新
*/
self.countNumber += 1;
dispatch_semaphore_signal(semaphore);
/*
* 排他控制处理结束,
* 所以通过 dispatch_semaphore_signal 函数
* 将 Dispatch Semaphore 的计数值加 1。
* 如果有通过 dispatch_semaphore_wait 函数
* 等待 Dispatch Semaphore 的计数值增加的线程,
* 就由最先等待的线程执行。
*/
NSLog(@"%d",self.countNumber);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 10000; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
self.countNumber = self.countNumber + 1;
dispatch_semaphore_signal(semaphore);
NSLog(@"%d",self.countNumber);
}
});
这样就保证了线程安全,最后的结果是20000。
在没有Serial Dispatch Queue和 dispatch_barrier_async
函数那么大粒度且一部分处理需要进行排他控制的情况下,Dispatch Semaphore 便可发挥威力。
《关于dispatch_semaphore的使用》中有这样的描述:
停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait
函数就相当于来了一辆车,dispatch_semaphore_signal
就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了dispatch_semaphore_create(long value)
,调用一次dispatch_semaphore_signal
,剩余的车位就增加一个;调用一次dispatch_semaphore_wait
剩余车位就减少一个;
当剩余车位为0时,再来车(即调用dispatch_semaphore_wait
)就只能等待。有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,所以就一直等下去。
Parse源码浅析系列(一)---Parse的底层多线程处理思路:GCD高级用法中这样描述:
在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。 更进一步,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait(等待)操作时,它要么通过然后将信号量减一,要么一直等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的资源。
从 iOS7 升到 iOS8 后,GCD 出现了一个重大的变化:在 iOS7 时,使用 GCD 的并行队列,dispatch_async
最大开启的线程一直能控制在6、7条,线程数都是个位数,然而 iOS8后,最大线程数一度可以达到40条、50条。然而在文档上并没有对这一做法的目的进行介绍。
笔者推测 Apple 的目的是想借此让开发者使用NSOperationQueue
:GCD 中 Apple 并没有提供控制并发数量的接口,而NSOperationQueue
有,如果需要使用 GCD 实现,需要使用 GCD 的一项高级功能:Dispatch Semaphore信号量。
接下来我们使用Dispatch Semaphore来控制GCD的并发数
设置并发数为 3,即最多有三条线程同时执行
- (void)viewDidLoad {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
dispatch_queue_t queue = dispatch_queue_create("com.example", DISPATCH_QUEUE_CONCURRENT);
unsigned int sleepTime = 2;
dispatch_async(queue, ^{
for (int i = 0; i < 1000; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%@",[NSThread currentThread]);
sleep(sleepTime);
dispatch_semaphore_signal(semaphore);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 1000; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%@",[NSThread currentThread]);
sleep(sleepTime);
dispatch_semaphore_signal(semaphore);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 1000; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%@",[NSThread currentThread]);
sleep(sleepTime);
dispatch_semaphore_signal(semaphore);
}
});
}
我们在代码中让每条线程每两秒执行一次,放慢速度后就容易看出同时有几条线程在执行
我们把
dispatch_semaphore_create()
参数换成 2,即dispatch_semaphore_create(2)
,其它代码原封不动,下面是运行结果由上述结果可以看出,我们给并发队列异步添加了3个任务,如果没有限制的情况下会创建3条子线程同时执行。当我们把
dispatch_semaphore_create()
参数设为 3 的时候,3 条线程的确同时执行,当我们换成 2 的时候,就只剩下两条线程在同时执行。这就验证了dispatch_semaphore_create()
的参数是可以控制并发数的说法。
所以,我们很多时候看到在网上各类博文中出现的dispatch_semaphore_create(1)
,这种情况大多被当做线程锁来使用是没有问题的。因为参数为1,所以同时执行的线程只能有1个,达到了线程锁要求的效果。按照dispatch_semaphore_create()
的原理,与自旋锁不同,却类似于互斥锁,具有线程的排他性。
网友评论