GCD 多线程编程 - iOS开发

前言

通过本文章你会到:

  • 多线程编程当中的基本概念
  • 如何防止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")
}
dispatch_sync_in_action_swift.gif

任务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")
}
dispatch_async_in_action_swift.gif

如图,任务viewDidLoad不等待异步asyn里面的代码任务执行完,就直接开始下一行代码

危险代码段(Critical Section)


这是一段不能并发执行的代码。也就是不能被两个线程同时执行。例如:这段代码是因为修改共享的资源例如(变量),只要多线程运行会立刻崩溃。

线程安全


能被多个线程同时执行的资源

资源竞争(race condition)


race-condition@2x-8b11b31d.png

如图,有一个变量存储着17,然后线程A读取变量得到17,线程B也读取了这个变量。两个线程都进行了加1运算得,并写入18都变量。从而导致了崩溃。这就是race condition,多线程使用共享资源,而没有确保其它线程是已经结束使用共享资源。

互相排除(Mutual Exclusion)


locking@2x-f425450b.png

如图,使用锁对线程正在使用的共享资源进行锁定,当共享资源使用完后。其它线程才能使用这个共享变量。这样就能避免race condition但会导致死锁。

死锁(Deadlock)


dead-lock@2x-b45f0acd.png

两个线程等待着彼此的完成而陷入的困境称为死锁。

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)


优先顺序颠倒问题,是由低权限任务的阻塞着高权限任务的执行。从而让顺序颠倒。

priority-inversion@2x-72e6760c.png

假设低权限的线程和高权限的线程使用共享资源。本应该,低权限的线程任务使用完共享资源后高权限的线程任务就能没有延迟地执行。

但由于一开始高权限的线程因为低线程的锁而受到阻塞。所以就给了机会中权限的线程任务,因为现在高权限受阻,所以中权限的线程任务是权限最高的,所以中权限任务中断低权限的线程任务的执行。从而让低线程的锁解不开,高线程任务也就延迟执行。从而优先顺序颠倒。

所以在使用GCD的时候,最好将多线程的任务执行优先权限保持一致。

队列(Queues)


GCD提供dispatch queue来处理提交的任务。这些队列管理你提交给GCD的任务并且按照FIFO(先入先出)的顺序执行。

dispatch queue都是线程安全的所以能够在多个线程里同时使用它们。

串行队列(Serial Queues)

Serial-Queue-Swift-480x272.png

串行队列只能一次执行一个任务。如图,串行队列会一个紧挨着一个地执行任务,就是一个结束另一个才能开始。

并发队列(Concurrent Queues)

Concurrent-Queue-Swift-480x272.png

并发队列里的任务是以进入队列的顺序执行的,但你不确定任务何时完成,何时开始下一个任务。这取决于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-Barrier-Swift-480x272.png

如图,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是有顺序的。从而避免多线程的读写问题。


参考链接
objc.io关于多线程可能导致的问题文章
raywenderlich关于GCD使用的文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容