#主要内容
到目前为止,我们已经了解了GCD和NSOperation在多线程编程中的使用。NSOperation是对GCD更高层次的封装,提供了任务的取消、暂停、恢复功能。但GCD因为更加接近底层,所以也有自己的优势。本章将会由浅入深,讨论以下几个部分:
dispatch_suspend
和dispatch_resume
dispathc_once
dispatch_barrier_async
dispatch_semaphore
#dispatch_suspend和dispatch_resume
我们知道NSOperationQueue
有暂停(suspend)和恢复(resume)。其实GCD中的队列也有类似的功能。用法也非常简单:
dispatch_suspend(queue) //暂停某个队列
dispatch_resume(queue) //恢复某个队列
这些函数不会影响到队列中已经执行的任务,队列暂停后,已经添加到队列中但还没有执行的任务不会执行,直到队列被恢复。
#dispathc_once
首先我们来看一下最简单的* dispathc_once
函数,这在单例模式中被广泛使用。
dispathc_once
函数可以确保某个block在应用程序执行的过程中只被处理一次,而且它是线程安全的。所以单例模式可以很简单的实现,以OC中Manager类为例
+ (Manager *)sharedInstance {
static Manager *sharedManagerInstance = nil;
static dispatch_once_t once;
dispatch_once($once, ^{
sharedManagerInstance = [[Manager alloc] init];
});
return sharedManagerInstance;
}
这段代码中我们创建一个值为nil的sharedManagerInstance
静态对象,然后把它的初始化代码放到dispatch_once
中完成。
这样,只有第一次调用sharedInstance
方法时才会进行对象的初始化,以后每次只是返回sharedManagerInstance
而已。
#dispatch_barrier_async
我们知道数据在写入时,不能在其他线程读取或写入。但是多个线程同时读取数据是没有问题的。所以我们可以把读取任务放入并行队列,把写入任务放入串行队列,并且保证写入任务执行过程中没有读取任务可以执行。
这样的需求比较常见,GCD提供了一个非常简单的解决办法——dispatch_barrier_async
假设我们有四个读取任务,在第二三个任务之间有一个写入任务,代码大概是这样:
let queue = dispatch_queue_create("com.gcd.kt", DISPATCH_QUEUE_CONCURRENT)
dispatch_async(queue, block1_for_reading)
dispatch_async(queue, block2_for_reading)
/*
这里插入写入任务,比如:
dispatch_async(queue, block_for_writing)
*/
dispatch_async(queue, block3_for_reading)
dispatch_async(queue, block4_for_reading)
如果代码这样写,由于这几个block是并发执行,就有可能在前两个block中读取到已经修改了的数据。如果是有多写入任务,那问题更严重,可能会有数据竞争。
如果使用dispatch_barrier_async
函数,代码就可以这么写:
dispatch_async(queue, block1_for_reading)
dispatch_async(queue, block2_for_reading)
dispatch_barrier_async(queue, block_for_writing)
dispatch_async(queue, block3_for_reading)
dispatch_async(queue, block4_for_reading)
dispatch_barrier_async
会把并行队列的运行周期分为这三个过程:
- 首先等目前追加到并行队列中所有任务都执行完成
- 开始执行
dispatch_barrier_async
中的任务,这时候即使向并行队列提交任务,也不会执行 dispatch_barrier_async
中的任务执行完成后,并行队列恢复正常。
总的来说,dispatch_barrier_async
起到了“承上启下”的作用。它保证此前的任务都先于自己执行,此后的任务也迟于自己执行。正如barrier的含义一样,它起到了一个栅栏、或是分水岭的作用。
这样一来,使用并行队列和dispatc_barrier_async
方法,就可以高效的进行数据和文件读写了。
dispatch_semaphore
首先介绍一下信号量(semaphore)的概念。信号量是持有计数的信号,不过这么解释等于没解释。我们举个生活中的例子来看看。
假设有一个房子,它对应进程的概念,房子里的人就对应着线程。一个进程可以包括多个线程。这个房子(进程)有很多资源,比如花园、客厅等,是所有人(线程)共享的。
但是有些地方,比如卧室,最多只有两个人能进去睡觉。怎么办呢,在卧室门口挂上两把钥匙。进去的人(线程)拿着钥匙进去,没有钥匙就不能进去,出来的时候把钥匙放回门口。
这时候,门口的钥匙数量就称为信号量(Semaphore)。很明显,信号量为0时需要等待,信号量不为零时,减去1而且不等待。
在GCD中,创建信号量的语法如下:
var semaphore = dispatch_semaphore_create(2)
这句代码通过dispatch_semaphore_create
方法创建一个信号量并设置初始值为2。然后就可以调用dispatch_semaphore_wait
方法了。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
dispatch_semaphore_wait
方法表示一直等待直到信号量的值大于等于一,当这个方法执行后,会把第一个信号量参数的值减1。
第二个参数是一个dispatch_time_t
类型的时间,它表示这个方法最大的等待时间。这在第一章中已经讲过,比如
DISPATCH_TIME_FOREVER
表示永久等待。
返回值也和dispatch_group_wait
方法一样,返回0表示在规定的等待时间内第一个参数信号量的值已经大于等于1,否则表示已超过规定等待时间,但信号量的值还是0。
dispatch_semaphore_wait
方法返回0,因为此时的信号量的值大于等于一,任务获得了可以执行的权限。这时候我们就可以安全的执行需要进行排他控制的任务了。
任务结束时还需要调用
dispatch_semaphore_signal()
方法,将信号量的值加1。这类似于之前所说的,从卧室出来要把锁放回门上,否则后来的人就无法进入了。
我们来看一个完整的例子:
var semaphore = dispatch_semaphore_create(1)
let queue = dispatch_queue_create("com.gcd.kt", DISPATCH_QUEUE_CONCURRENT)
var array: [Int] = []
for i in 1...100000 {
dispatch_async(queue, { () -> Void in
/*
某个线程执行到这里,如果信号量值为1,那么wait方法返回1,开始执行接下来的操作。
与此同时,因为信号量变为0,其它执行到这里的线程都必须等待
*/
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
/*
执行了wait方法后,信号量的值变成了0。可以进行接下来的操作。
这时候其它线程都得等待wait方法返回。
可以对array修改的线程在任意时刻都只有一个,可以安全的修改array
*/
array.append(i)
/*
排他操作执行结束,记得要调用signal方法,把信号量的值加1。
这样,如果有别的线程在等待wait函数返回,就由最先等待的线程执行。
*/
dispatch_semaphore_signal(semaphore)
})
}
如果你想知道不用信号量会出什么问题,可以看我的另一篇文章Swift数组append方法研究