Swift 并发编程(一)GCD篇

引言

今天从Books中翻出了“沉淀”已久的关于并发编程书,读完之后,感受颇多,有一些不确定的知识点更加清晰了。在此,结合自己的开发经验,做一个总结。主要从两个方面入手:GCD和Operation,也是目前比较主流的实现并发的方式。

GCD 和 Operation的区别和抉择

GCD是基于libdispatch实现的,目的是为了降低并发的成本;最大的特点是大量使用了block,使多线程的实现变得简单。Operation是基于GCD的封装,更具有封装性,具备GCD不能实现的功能,比如cancel任务。

如何选择?

选用GCD:简单任务,且代码不会被重用;

选用Operation:代码适合封装起来重用;多层异步嵌套(如异步A回调B,B回调C...如果用GCD会造成多层block嵌套,可读性太差);有cancel操作。

不要混淆队列和Thread

先来看一个面试经常遇到的考题:

dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{  // 这个地方会阻塞主线程
print("hello");
});

首先队列不是线程,队列是一个FIFO的数据结构;创建一个队列,系统会根据系统的使用情况创建一个或者多个线程来配合这个队列工作。线程是实实在在运行逻辑的。你可以抽象成这个样子:


1.png

根据图来分析: 主线程执行主队列中的print("Hello")操作,而且是同步的,此时主线程等待,主队列任务开始执行,但是主队列的Task需要在主线程完成,这样就出现了两者互相等待的情况,所以造成死锁。所以请一定记住“队列不等于线程”

GCD常见用法

  1. 创建同步队列

    let label = "com.piplab.queue.identifer"
    let queue = DispatchQueue(label: label)
    

    label是队列的唯一标识 ,当队列创建后,OS会选择性的为队列创建或分配一个或多个线程;如果有可复用的线程,就复用线程;否则,将根据情况创建。

    主队列比较特殊,App启动时创建,并且是用来处理UI相关操作的同步队列。切记永远不要同步主队列进行一些除UI相关的耗时操作。

  2. 创建并发队列

    let label = "com.piplab.queue.concurrent"
    let queue = DispatchQueue(label: label, attributes: .current)
    

    除了自定义外,系统已经预定义了6种不同优先级的并发​队列,下面会讲到。

  3. 队列优先级

    按照优先级从高到低依次是:

    • .userInteractive: 一般用于用户交互,需要快速响应的情况。
    • .userInitiated: 一般用户用户交互之后,需要迅速异步操作的情况,比如用户需要读取数据库,需要快速响应,读取数据。
    • .default: 系统默认优先级。不要直接使用,否则会出现不必要的错误
    • .utility :一般用于进度条,IO,网络请求等情况。系统会根据电池情况,平衡响应频率
    • .background: 一般用户用户不需要感知的情况。
    • .unspecified: 不建议使用。

    这六种优先级并发队列,系统都有预定义。但是并不意味着我们不能创建不同优先级的队列。

    let label = "com.piplab.quality"
    let queue = DispatchQueue(label: label, qos: .userInteractive, attributes: .current)
    

    注意: Queue的优先级并不是一味不变的。如果将一个高优先级的Task交给一个低优先级的Queue,那么Queue的优先级会跟着提升,并且Queue中的其他Task的优先级也会跟着提升

  4. 派发任务

    DispatchQueue.global(qos: .utility).async { [weak self] in
      guard let self = self else { return }
       // do something
      // Switch back to the main queue to
      // update your UI
      DispatchQueue.main.async {
        self.textLabel.text = "New articles available!"
      }
    }
    

    虽然在block中不声明 [weak self] 也不会造成循环引用,但是会延长self的声明周期,直到block执行完毕。在切换到主队列时,尽量做比较少的工作。

  5. DispatchGroup

    适用于“当一组任务完成后,再执行特定的任务,组任务并发执行”

    let group = DispatchGroup()
    
    someQueue.async(group: group) { ... your work ... }                  //任务1
    someQueue.async(group: group) { ... more work .... }                 //任务2
    someOtherQueue.async(group: group) { ... other work ... }    //任务3
    
    group.notify(queue: DispatchQueue.main) { [weak self] in
       self?.textLabel.text = "All jobs have completed"                  //任务4
    }
    

    值的注意的是一个group里的Task,可以分配到不同的Queue。 任务4会在任务1,2,3 都执行完之后再执行,如果任务1,2,3中有一直执行不完,那么任务4是不会执行的。

    如果不能等待所有的任务执行完,还可以用以下方式实现:

    let group = DispatchGroup()
    
    someQueue.async(group: group) { ... }
    someQueue.async(group: group) { ... }
    someOtherQueue.async(group: group) { ... } 
    
    if group.wait(timeout: .now() + 60) == .timedOut {
      print("The jobs didn't finish in 60 seconds")
    } else {
      print ("All jobs have complete")
    }
    

    切记不要在主线程调用group.wait方法.

    对于多层异步嵌套的情况,group还有另外一种实现方式:

    queue.dispatch(group: group) {
      // count is 1
      group.enter()
      // count is 2
      someAsyncMethod { 
        defer { group.leave() }
        
        // Perform your work here,
        // count goes back to 1 once complete
      }
    }
    
  6. 信号量Semaphores

    简单的理解信号量 就是一个计数器,每次使用时 -1 ,为0时,代表资源不够,线程等待,释放时 +1 。一个简单的例子: 批量下载图片,最多4个线程同时下载,那么4可以定义为一个信号量。

    let semaphore = DispatchSemaphore(value: 4)
    semaphore.wait()  //信号量-1,如果为0,则等待
    semaphore.signal() //信号量 +1
    
  7. 并发三大难题

    • 资源竞争:在多个线程“同时写” 的情况,容易出现错误。
      2.png
 假设有一个值value为1,别用两个线程各使value的值+1,正确结果应该是3,但实际效果是:value的值为1,第一个时钟,Thread1读取的值是1;第二个时钟Thread1将value的值修改为2,因为Thread1还没有写回到value中,所以Thread2读取的值是1,第三个时钟Thread1将2写回到value中,thread2将value值+1修改为2,第四个时钟,Thread2将2写回到value中,这样结果为2,所以是错误的。正确的做法是将“+1”操作和写操作同步执行。

 `serialQueue.async{ value +1 ,write()}`

  下面说一下并发情况下变量访问安全措施(这是另外一个问题,跟上面无关哦)。

 当多个线程访问同意变量时,为了避免读的同时写的问题,如图:
3.png

实现如下:

 ```swift
 private let threadSafeCountQueue = DispatchQueue(label: "...", attributes: .concurrent)
 private var _count = 0
 public var count: Int {
   get {
     return threadSafeCountQueue.sync {
       return _count
     }
   }
   set {
     threadSafeCountQueue.async(flags: .barrier) { [unowned self] in
       self._count = newValue
     }
   }
 }
 ```
  • 死锁问题:简而言之就是TaskA拥有资源A等待资源B,TaskB 拥有资源B,等待资源A,两者互相等待,谁也拿不到想要的资源。形象点表示,如下图:


    4.png

    避免这个问题的方法只有一个:每个线程按照统一的顺序申请资源。比如:TaskA需要资源1和资源2,那么TaskB也按照同样的顺序申请资源1和资源2,就不会出现这个问题。

  • 优先级反转

    这种情况表现为“低优先级的Task拥有高优先级需要的资源,导致高优先级不能执行”。试想一下,一个不着急上厕所的人,拿着所有的纸,即使你再着急,你也没办法啊。造成的原因是“一个低优先级的队列的优先级比高优先级队列的优先级还高”。还记得创建Queue的时候有个优先级的参数吗?DispatchQueue(..., qos: .userInteractive,...) ,还有分配任务时也有一个优先级参数queue.async(..., qos: .userInteractive,...)``,如果任务的优先级高于队列的优先级,就可能出现这种情况。解决的方法很简单:“任务的优先级不能高于队列”

最后

相信GCD在大家的日常开发中经常用到,一些常见的用法也早已掌握。文中提到的一些注意点是我个人曾经遇到的坎儿,希望对大家有所帮助。如果有不对的地方,欢迎留言指正。不知不觉已经凌晨了,先到这儿,明天写一下Operation。最后悄悄写一句“如果喜欢,欢迎打赏”

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