前言
通过本文章你会学到:
- 多线程编程当中的基本概念
- 如何防止GCD多线程的读写问题
简介
GCD是Grand Central Dispatch的简称。为什么使用GCD?
使用GCD具有以下好处:
- GCD能改善app的响应能力,通过将一些比较耗费时间的任务(tasks)运行在后台(background)
- GCD提供一个比较容易使用的并发模型。避免一些并发引起的Bug
为了更好地使用GCD,需要了解以下的关于线程和并发的概念。
串行(Serial) vs 并发(Concurrent)
这两个词都是形容当执行任务时是否需要考虑其它任务。
串行:任务只能一个接着一个地执行
并发:同一时间内,多个任务可以是同时执行的
任务(Tasks)
可以简单地认为任务就是闭包(closure)。实际上,也可以通过函数指针来使用GCD,但大多数情况下会比较麻烦。
闭包就是一段可以被存储并传值的可调用代码块。
同步(Synchronous) vs 异步(Asychronous)
这两个词是用来形容,被调用的函数在什么时候将控制权返回给调用者。
同步:被调用的函数只会在执行完才将控制权给回调用者。
异步:被调用的函数会马上将控制权返回给调用者,不管函数是否执行完。因此异步函数不会阻塞当前进程。
例如以下代码:
同步
override func viewDidLoad() {
super.viewDidLoad()
dispatch_sync(dispatch_get_global_queue(
Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {
NSLog("First Log")
}
NSLog("Second Log")
}
任务viewDidLoad会暂停等同步sync里面的任务执行完再继续下一行代码。
异步
override func viewDidLoad() {
super.viewDidLoad()
dispatch_async(dispatch_get_global_queue(
Int(QOS_CLASS_USER_INTERACTIVE.value), 0)) {
NSLog("First Log")
}
NSLog("Second Log")
}
如图,任务viewDidLoad不等待异步asyn里面的代码任务执行完,就直接开始下一行代码
危险代码段(Critical Section)
这是一段不能并发执行的代码。也就是不能被两个线程同时执行。例如:这段代码是因为修改共享的资源例如(变量),只要多线程运行会立刻崩溃。
线程安全
能被多个线程同时执行的资源
资源竞争(race condition)
如图,有一个变量存储着17,然后线程A读取变量得到17,线程B也读取了这个变量。两个线程都进行了加1运算得,并写入18都变量。从而导致了崩溃。这就是race condition,多线程使用共享资源,而没有确保其它线程是已经结束使用共享资源。
互相排除(Mutual Exclusion)
如图,使用锁对线程正在使用的共享资源进行锁定,当共享资源使用完后。其它线程才能使用这个共享变量。这样就能避免race condition但会导致死锁。
死锁(Deadlock)
两个线程等待着彼此的完成而陷入的困境称为死锁。
void swap(A, B)
{
lock(lockA);
lock(lockB);
int a = A;
int b = B;
A = b;
B = a;
unlock(lockB);
unlock(lockA);
}
swap(X, Y); // 线程 thread 1
swap(Y, X); // 线程 thread 2
结果就会导致X被线程1锁住,Y被线程2锁住。而线程1又不能使用Y直到线程2解锁,同理,线程2也不能使用X。这就是死锁,互相等待。
优先顺序颠倒(Priority Inversion)
优先顺序颠倒问题,是由低权限任务的阻塞着高权限任务的执行。从而让顺序颠倒。
假设低权限的线程和高权限的线程使用共享资源。本应该,低权限的线程任务使用完共享资源后高权限的线程任务就能没有延迟地执行。
但由于一开始高权限的线程因为低线程的锁而受到阻塞。所以就给了机会中权限的线程任务,因为现在高权限受阻,所以中权限的线程任务是权限最高的,所以中权限任务中断低权限的线程任务的执行。从而让低线程的锁解不开,高线程任务也就延迟执行。从而优先顺序颠倒。
所以在使用GCD的时候,最好将多线程的任务执行优先权限保持一致。
队列(Queues)
GCD提供dispatch queue来处理提交的任务。这些队列管理你提交给GCD的任务并且按照FIFO(先入先出)的顺序执行。
dispatch queue都是线程安全的所以能够在多个线程里同时使用它们。
串行队列(Serial Queues)
串行队列只能一次执行一个任务。如图,串行队列会一个紧挨着一个地执行任务,就是一个结束另一个才能开始。
并发队列(Concurrent Queues)
并发队列里的任务是以进入队列的顺序执行的,但你不确定任务何时完成,何时开始下一个任务。这取决于GCD。
如图:任务0,1,2,3的开始是有顺序的。但同一时间内可以有多个任务运行。
队列类型(Queue Types)
首先,系统提供了个特效的队列main queue,
main queue是串行队列,所以一次只能执行一个任务。但这个队列是唯一一个能用来更新UI和发送通知的队列。
系统也同时提供了几个并发队列。这些队列与自身的QOS等级有关,使得GCD可以决定优先级。
- QOS_CLASS_USER_INTERACTIVE
- QOS_CLASS_USER_INITIATED
- QOS_CLASS_UTILITY
- QOS_CLASS_BACKGROUND
Apples的API也会调用这些global dispatch queues,所以你添加的任务不会是队列里唯一的任务。
除了以上5种队列外,你还可以创建队列。
防止read write问题
譬如在一个单例里有有如下属性,读和写
private var _photos: [Photo] = []
//write
func addPhoto(photo: Photo) {
_photos.append(photo)
}
//read
var photos: [Photo] {
return _photos
}
能够确保线程安全的只有用let定义的。而数组,字典等用var定义的都不是线程安全的。
而上面的read和write都对变量数组进行更改使用。所以线程不安全。那怎么才能使其线程安全呢?
利用dispatch barriers
实施read write lock
如图,dispatch barriers 能确保在它之前的task都执行完,在它之后的task 直到它完成前都不能执行也就是说在这个task在它执行的时刻独占所在队列。
那么修改后的read write如下:
private var _photos: [Photo] = []
//创建自定义并发队列
private let concurrentPhotoQueue = dispatch_queue_create(
"com.raywenderlich.GooglyPuff.photoQueue", DISPATCH_QUEUE_CONCURRENT)
//write
func addPhoto(photo: Photo) {
dispatch_barrier_async(concurrentPhotoQueue) {
self._photos.append(photo)
}
}
//read
var photos: [Photo] {
var photosCopy: [Photo]!
dispatch_sync(concurrentPhotoQueue) {
photosCopy = self._photos
}
return photosCopy
}
write:
使用dispatch barriers,为什么创建自定义并发队列?
因为能够使用dispatch barriers有以下三种队列:
- 自定义串行队列,因为串行队列本来就是一个任务一个任务地执行,所以用了等于没用。
- 全局队列(Global Concurrent Queue),因为全局队列,Apples的API也使用,所以当使用dispatch barriers时,会阻塞队列。不建议使用
- 自定义并发队列,只要并发队列里使用的是资源线程安全的。所以比较建议。
read:
使用sync并且跟write同一并发队列,因为这确保read和write是有顺序的。从而避免多线程的读写问题。