iOS 多线程开发之 GCD

222 阅读23分钟

iOS 多线程开发之系列文章

iOS 多线程开发之概念

iOS 多线程开发之 Thread

iOS 多线程开发之 GCD

iOS 多线程开发之 Operation

iOS 多线程开发之线程安全

多线程开发是日常开发任务中不可缺少的一部分,在 iOS 开发中常用到的多线程开发技术有 GCD、OperationThread,本文主要讲解多线系列文章中关于 GCD 的相关知识和使用详解。

基础

Grand Central Dispatch 简称 GCD,苹果官方推荐给开发者使用的首选多线程解决方案。多线程开发涉及的细节非常多,下面我会用例子细致的讲解 GCD,请一定要精读,一定要用 Xcode 或 Playground 多次运行代码去对比结果。实践出真知,练习完这篇文章,你一定会觉得精通 Swift 多线程原来很简单。

DispatchWorkItem

调度工作项:其实就是一项任务,可以把你想要执行的代码写成闭包,在DispatchWorkItem初始化时传进去,方便后续管理任务,并且会让代码更整洁。

官网原文:The work you want to perform, encapsulated in a way that lets you attach a completion handle or execution dependencies.

调度工作项初始化,正常情况下,使用第一种方式即可(特殊情况后续会再讲解):

// 1. 只带尾随闭包
let item1 = DispatchWorkItem {
    print("item1")
}

// 2. 指定 qos(执行优先级)或 flags(特殊行为标记)
let item2 = DispatchWorkItem(qos: .userInteractive, flags: .barrier) {
    print("item2")
}

DispatchQueue

调度队列:一个对象,用来管理任务在 app 的主线程或后台线程串行或并行执行。

官网原文:An object that manages the execution of tasks serially or concurrently on your app's main thread or on a background thread.

DispatchQueue 有三种类型:

  • Main Queue
  • Global Queue
  • Custom Queue

Main Queue(主队列,串行)

Main Queue 与主线程关联的调度队列,是一种串行队列(Serial),与 UI 相关的操作必须放在 Main Queue 中执行,获取方式是:

let mainQueue = DispatchQueue.main

Global Queue(全局队列,并行)

Global Queue 运行在后台线程,是系统内共享的全局队列,是一种并行队列(Concurrent),用于处理并发任务,获取方式是:

let globalQueue = DispatchQueue.global()

Custom Queue(自定义队列,默认串行)

Custom Queue 运行在后台线程,默认是串行队列(Serial),初始化时指定 attributes 参数为 .concurrent ,可以创建成并行队列(Concurrent),创建方式如下:

//串行队列,label名字随便取
let serialQueue = DispatchQueue(label: "test")

//并行队列
let concurrentQueue = DispatchQueue(label: "test", attributes: .concurrent)

DispatchGroup

调度组:一个小组,你可以把多项任务放到一个组里,方便进行统一管理(直译过来并不好理解)。

官网原文:A group of tasks that you monitor as a single unit.

DispatchGroup 可以很方便的管理多项任务。比如当同一组里的所有事件都完成后,GCD API 可以发送通知,执行相应的操作。常用方法:

  • notify()

    调度组里的所有任务执行完毕,会在此收到通知,不会阻塞当前线程。

  • wait()

    一直等待,直到调度组里所有任务都执行完毕或等待超时,阻塞当前线程。

实战

使用 DispatchQueue

新建 Playground 项目,定义四个调度任务,提供给下文调用,可大幅降低下文代码量,部分运行结果请自己复制代码多次运行感受,我只讲结果:

import Foundation

//定义四个调度任务,打印当前线程数据
let item1 = DispatchWorkItem {
    for i in 0...4{
        print("item1 -> (i)  thread: (Thread.current)")
    }
}

let item2 = DispatchWorkItem {
    for i in 0...4{
        print("item2 -> (i)  thread: (Thread.current)")
    }
}

let item3 = DispatchWorkItem {
    for i in 0...4{
        print("item3 -> (i)  thread: (Thread.current)")
    }
}

let item4 = DispatchWorkItem {
    for i in 0...4{
        print("item4 -> (i)  thread: (Thread.current)")
    }
}

异步执行

//主队列追加异步任务,按顺序打印
let mainQueue = DispatchQueue.main
mainQueue.async(execute: item1)
mainQueue.async(execute: item2)
mainQueue.async(execute: item3)
mainQueue.async(execute: item4)

//全局队列追加异步任务,随机打印
let globalQueue = DispatchQueue.global()
globalQueue.async(execute: item1)
globalQueue.async(execute: item2)
globalQueue.async(execute: item3)
globalQueue.async(execute: item4)

//自定义串行队列追加异步任务,按顺序打印
let serialQueue = DispatchQueue(label: "serial")
serialQueue.async(execute: item1)
serialQueue.async(execute: item2)
serialQueue.async(execute: item3)
serialQueue.async(execute: item4)

//自定义并行队列追加异步任务,随机打印
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
concurrentQueue.async(execute: item1)
concurrentQueue.async(execute: item2)
concurrentQueue.async(execute: item3)
concurrentQueue.async(execute: item4)

注:在串行队列中执行异步任务,结果跟执行同步任务完全一样

同步执行

//主队列追加同步任务,会引起死锁
let mainQueue = DispatchQueue.main
mainQueue.sync(execute: item1)
mainQueue.sync(execute: item2)
mainQueue.sync(execute: item3)
mainQueue.sync(execute: item4)

//全局队列追加同步任务,按顺序打印
let globalQueue = DispatchQueue.global()
globalQueue.sync(execute: item1)
globalQueue.sync(execute: item2)
globalQueue.sync(execute: item3)
globalQueue.sync(execute: item4)

//自定义串行队列追加同步任务,按顺序打印
let serialQueue = DispatchQueue(label: "serial")
serialQueue.sync(execute: item1)
serialQueue.sync(execute: item2)
serialQueue.sync(execute: item3)
serialQueue.sync(execute: item4)

//自定义并行队列追加同步任务,按顺序打印
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
concurrentQueue.sync(execute: item1)
concurrentQueue.sync(execute: item2)
concurrentQueue.sync(execute: item3)
concurrentQueue.sync(execute: item4)

注:在并行队列中执行同步任务,跟在串行队列中执行异步或同步任务,结果完全一样。 在主队列中不能混入同步任务,否则会引起死锁。

同步异步混合执行

//主队列同步异步混合,会引起死锁
let mainQueue = DispatchQueue.main
mainQueue.sync(execute: item1)//同步任务
mainQueue.async(execute: item2)
mainQueue.async(execute: item3)
mainQueue.async(execute: item4)

//全局队列同步异步混合,同步任务按顺序打印,异步任务随机打印
//本例中同步任务执行完,才会执行后续的异步任务
let globalQueue = DispatchQueue.global()
globalQueue.sync(execute: item1)//同步任务
globalQueue.async(execute: item2)
globalQueue.async(execute: item3)
globalQueue.async(execute: item4)

//自定义串行队列同步异步混合,按顺序打印
let serialQueue = DispatchQueue(label: "serial")
serialQueue.sync(execute: item1)//同步任务
serialQueue.async(execute: item2)
serialQueue.async(execute: item3)
serialQueue.async(execute: item4)

//自定义并行队列同步异步混合,同步任务按顺序打印,异步任务随机打印
//本例中同步任务执行完,才会执行后续的异步任务
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
concurrentQueue.sync(execute: item1)//同步任务
concurrentQueue.async(execute: item2)
concurrentQueue.async(execute: item3)
concurrentQueue.async(execute: item4)

注:在并行队列中执行同步任务,跟在串行队列中执行异步或同步任务,结果完全一样。 在主队列中不能混入同步任务,否则会引起死锁。

并发队列手动创建的串行队列主队列
同步(sync)没有开启新线程,串行执行任务没有开启新线程,串行执行任务没有开启新线程,串行执行任务,会引起死锁
异步(async)有开启新线程,并发执行任务有开启新线程,串行执行任务没有开启新线程,串行执行任务

死锁分析

主队列死锁

上文提到了主队列不能混入同步任务,否则会引起死锁,为何呢?因为主队列是串行队列,并且仅能运行在主线程上,它无法去创建新的线程,也就意味着所有的代码都必须在只能在一个线程上运行。
正常情况下,主队列上存在源源不断的异步任务(比如用来不断刷新 UI 的任务,用 A 表示),如果混入同步任务(用 B 表示),如果 B 在 A 之后,从时间上看,B 执行完才能执行 A;而从空间上看,A 执行完才能执行 B。两个任务都很有礼貌,相互等待、相互谦让,谁也不好意思先执行,于是就引起了死锁,导致程序卡死崩溃。

官网原文:Attempting to synchronously execute a work item on the main queue results in deadlock.

//会引起死锁
let mainQueue = DispatchQueue.main
mainQueue.async(execute: item1)
mainQueue.async(execute: item2)
mainQueue.async(execute: item3)
mainQueue.sync(execute: item4)//同步任务

有人可能会想,如果 A 在 B 之后呢?是不是就不会引起死锁?看起来不会死锁,可惜 Playground 运行这样的代码,每次都崩溃,应该是程序刚运行,主队列就存在我们看不到的异步任务。

//依然会引起死锁
let mainQueue = DispatchQueue.main
mainQueue.sync(execute: item1)//同步任务
mainQueue.async(execute: item2)
mainQueue.async(execute: item3)
mainQueue.async(execute: item4)

因此只能认为:主队列上不能存在同步任务,否则一定会引起死锁。

其他队列死锁

上文提到主队列死锁,那其他类型的队列会不会引起死锁呢?下面来试一下:

  • 自定义串行队列嵌套同步任务,会引起死锁
let serialQueue = DispatchQueue(label: "serial")
//死锁
serialQueue.sync {
    print("同步执行  thread: (Thread.current)")
    serialQueue.sync {
        print("同步执行  thread: (Thread.current)")
    }
}
//死锁
serialQueue.async {
    print("异步执行  thread: (Thread.current)")
    serialQueue.sync {
        print("同步执行  thread: (Thread.current)")
    }
}
//不会引起死锁
serialQueue.sync {
    print("同步执行  thread: (Thread.current)")
    serialQueue.async {
        print("异步执行  thread: (Thread.current)")
    }
}
//不会引起死锁
serialQueue.async {
    print("异步执行  thread: (Thread.current)")
    serialQueue.async {
        print("异步执行  thread: (Thread.current)")
    }
}
  • 并行队列嵌套同步任务,不会引起死锁
//自定义并行队列(全局并行队列结果一样)
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
//不会引起死锁
concurrentQueue.async {
    print("异步执行  thread: (Thread.current)")
    concurrentQueue.sync {
        print("同步执行  thread: (Thread.current)")
    }
}
//不会引起死锁
concurrentQueue.sync {
    print("同步执行  thread: (Thread.current)")
    concurrentQueue.sync {
        print("同步执行  thread: (Thread.current)")
    }
}
//不会引起死锁
concurrentQueue.sync {
    print("同步执行  thread: (Thread.current)")
    concurrentQueue.async {
        print("异步执行  thread: (Thread.current)")
    }
}
//不会引起死锁
concurrentQueue.async {
    print("异步执行  thread: (Thread.current)")
    concurrentQueue.async {
        print("异步执行  thread: (Thread.current)")
    }
}

死锁总结

通过上文可以看到,自定义串行队列嵌套同步任务,也是可以引起死锁的,所以死锁不是主队列的专利。但为什么会引起死锁,核心原因是什么?运行下面的代码看看结果:

print("=> 开始执行")

let mainQueue = DispatchQueue.main
mainQueue.async(execute: item1)//异步任务

print("=> 执行完毕1")

let globalQueue = DispatchQueue.global()
globalQueue.sync(execute: item2)//同步任务

print("=> 执行完毕2")

let serialQueue = DispatchQueue(label: "serial")
serialQueue.sync(execute: item3)//同步任务

print("=> 执行完毕3")

let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
concurrentQueue.sync(execute: item4)//同步任务

print("=> 执行完毕all")

运行结果:

=> 开始执行
=> 执行完毕1
item2 -> 0  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item2 -> 1  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item2 -> 2  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item2 -> 3  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item2 -> 4  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
=> 执行完毕2
item3 -> 0  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item3 -> 1  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item3 -> 2  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item3 -> 3  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item3 -> 4  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
=> 执行完毕3
item4 -> 0  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item4 -> 1  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item4 -> 2  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item4 -> 3  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item4 -> 4  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
=> 执行完毕all
item1 -> 0  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item1 -> 1  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item1 -> 2  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item1 -> 3  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}
item1 -> 4  thread: <NSThread: 0x7fbf2cc0e7e0>{number = 1, name = main}

看出什么问题没有?四组代码的运行结果完全一样,连线程信息也都一摸一样,都是运行在主线程上(main thread),并且第一组的代码放在了最后执行。也就是说:

  • 主队列上的所有任务(只有可能是异步任务)和其他队列的同步任务都运行在主线程上(主线程有且只有一个)。
  • 线程不在乎任务是同步还是异步,只有队列才在乎。
  • 线程不会死锁,只有队列才会死锁。

主队列添加同步任务会造成死锁的根本原因是:

  • 主队列只能运行在主线程(重要的事情再说一遍)。
  • 主队列没有本事开启后台线程去干别的事情。
  • 主队列一旦混入同步任务,就会跟已经存在的异步任务相互等待,导致死锁。

自定义串行队列添加同步任务不会死锁,因为:

自定义串行队列有能力启动主线程和后台线程(只能启动一个后台线程)。 自定义串行队列遇到同步任务,会自动安排在主线程执行;遇到异步任务,自动安排在后台线程执行,所以不会死锁。

并行队列添加同步任务不会死锁,因为:

并行队列有能力启动主线程和后台线程(可以启动一个或多个后台线程,部分设备上可以启动多达 64 个后台线程)。 并行队列遇到同步任务,会自动安排在主线程执行;遇到异步任务,自动安排在后台线程执行,所以不会死锁。

自定义串行队列一个异步或同步任务(A)嵌套另一个同步任务(B)会引起死锁,因为:

A、B 任务等效为:A1 -> B -> A2,B是同步任务,B 在 A1 之后、A2 之前,B 必须等 A2 执行完才能执行,A2 必须等 B 执行完才能执行,A2 执行完才算 A 执行完了,逻辑上已经陷入死循环,两者相互等待,导致死锁。所以,串行队列不能嵌套同步任务,否则会引起死锁。

DispatchQueue切换

背景介绍

这一章来模拟网络请求:在 APP 中请求网络数据(任务 A: 耗时 10s),获取数据后进行一定的处理(任务 B: 耗时 5s),最后刷新 UI。

假如A和B都是同步任务,放主队列会死锁,而放其他任何队列,界面都会卡死 15s,如果不信,把下面代码里的两种线程休眠方法(二选一,其实不止这两种),放在 APP UIViewController 里试试:

override func viewDidAppear(_ animated: Bool) {
    //1. 全局队列执行同步任务
    DispatchQueue.global().sync {
        sleep(15)//当前线程休眠15秒
    }
    //2. 主队列执行异步任务
    DispatchQueue.main.async {
        sleep(15)//当前线程休眠15秒
    }
}

不出所料,两种方法,均让界面卡死 15s。回想一下上文说过的:所有的同步任务最终都要安排到主线程运行,主线程运行长耗时任务都会导致界面严重卡顿,所以:

能异步执行的长耗时任务,千万不要同步执行。 长耗时同步任务欠下的债,都由界面来偿还。

假如 A 和 B 都是异步任务,即使这样,你也不能都放在主队列中处理,这样也会导致 APP 界面卡住 15s,因为上面说到了:主线程运行长耗时任务都会导致界面严重卡顿。

所有的长耗时任务,千万不要放在主队列中执行。 主队列长耗时异步任务欠下的债,也都由界面来偿还。

说了那么多,你现在应该能够深切地理解各种队列的运行原理了。

网络请求实例

现在讲讲使用 GCD 多线程处理网络请求的正确做法:A、B 都定义成异步任务,在并行队列中嵌套异步任务,最后切换到主队列去刷新 UI,这样做界面可以保证最流畅。

//创建并行队列,尽量用自定义队列,免得自己的代码质量不过关,影响全局队列
let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)

//异步执行
queue.async {
    
    print("开始请求数据 (Date())  thread: (Thread.current)")
    sleep(10)//模拟网络请求
    print("数据请求完成 (Date())  thread: (Thread.current)")
    
    //异步执行
    queue.async {
        print("开始处理数据 (Date())  thread: (Thread.current)")
        sleep(5)//模拟数据处理
        print("数据处理完成 (Date())  thread: (Thread.current)")
        
        //切换到主队列,刷新UI
        DispatchQueue.main.async {
            print("UI刷新成功  (Date())  thread: (Thread.current)")
        }
    }
}

运行结果:

开始请求数据 2020-08-06 06:40:57 +0000  thread: <NSThread: 0x7ff917d8c0c0>{number = 4, name = (null)}
数据请求完成 2020-08-06 06:41:07 +0000  thread: <NSThread: 0x7ff917d8c0c0>{number = 4, name = (null)}
开始处理数据 2020-08-06 06:41:07 +0000  thread: <NSThread: 0x7ff8f7d0c190>{number = 3, name = (null)}
数据处理完成 2020-08-06 06:41:12 +0000  thread: <NSThread: 0x7ff8f7d0c190>{number = 3, name = (null)}
UI刷新成功  2020-08-06 06:41:12 +0000  thread: <NSThread: 0x7ff917c0e7e0>{number = 1, name = main}

可以看到队列和线程均进行了预期的切换,GCD 队列切换像俄罗斯套娃一样,一层一层的嵌套就行,等嵌套出问题了,滑到上面在 死锁分析 里寻找原因进行修改即可。

使用 DispatchGroup

如果希望多项任务执行完毕后,再去执行另一项任务,可以使用DispatchGroup。这些任务可以放在同一队列中,也可以放在不同队列中。

DispatchGroup常用的方法:

  • group.wait()

    阻塞当前线程,一直到 group 所有任务执行完毕。

  • group.notify()

    所有任务执行完毕后,异步发送通知,不阻塞当前线程。

使用 group.notify() 改写一下上一章网络请求的例子:

let group = DispatchGroup()
let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)

//异步执行
queue.async(group: group) {
    
    print("开始请求数据 (Date())  thread: (Thread.current)")
    sleep(10)//模拟网络请求
    print("数据请求完成 (Date())  thread: (Thread.current)")
    
    //异步执行
    queue.async(group: group) {
        print("开始处理数据 (Date())  thread: (Thread.current)")
        sleep(5)//模拟数据处理
        print("数据处理完成 (Date())  thread: (Thread.current)")
    }
}

print("开始监听")

//在当前队列监听
group.notify(queue: queue) {
    //切换到主队列,刷新UI
    DispatchQueue.main.async {
        print("UI刷新成功  (Date())  thread: (Thread.current)")
    }
}

print("监听完毕")

运行结果:

开始监听
监听完毕
开始请求数据 2020-08-06 06:45:22 +0000  thread: <NSThread: 0x7fe312f30b60>{number = 4, name = (null)}
数据请求完成 2020-08-06 06:45:32 +0000  thread: <NSThread: 0x7fe312f30b60>{number = 4, name = (null)}
开始处理数据 2020-08-06 06:45:32 +0000  thread: <NSThread: 0x7fe312e70d70>{number = 5, name = (null)}
数据处理完成 2020-08-06 06:45:37 +0000  thread: <NSThread: 0x7fe312e70d70>{number = 5, name = (null)}
UI刷新成功  2020-08-06 06:45:37 +0000  thread: <NSThread: 0x7fe312c0e7e0>{number = 1, name = main}

如你所愿,运行结果跟上文一致。

精简代码,直接在主队列监听通知、刷新 UI:

let group = DispatchGroup()
let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)

//异步执行
queue.async(group: group) {
    
    print("开始请求数据 (Date())  thread: (Thread.current)")
    sleep(10)//模拟网络请求
    print("数据请求完成 (Date())  thread: (Thread.current)")
    
    //异步执行
    queue.async(group: group) {
        print("开始处理数据 (Date())  thread: (Thread.current)")
        sleep(5)//模拟数据处理
        print("数据处理完成 (Date())  thread: (Thread.current)")
    }
}

print("开始监听")

//切换到主队列监听,刷新UI
group.notify(queue: DispatchQueue.main) {
    print("UI刷新成功  (Date())  thread: (Thread.current)")
}

print("监听完毕")

运行结果:

开始监听
监听完毕
开始请求数据 2020-08-06 06:49:31 +0000  thread: <NSThread: 0x7fc608c80370>{number = 4, name = (null)}
数据请求完成 2020-08-06 06:49:41 +0000  thread: <NSThread: 0x7fc608c80370>{number = 4, name = (null)}
开始处理数据 2020-08-06 06:49:41 +0000  thread: <NSThread: 0x7fc608d2b200>{number = 5, name = (null)}
数据处理完成 2020-08-06 06:49:46 +0000  thread: <NSThread: 0x7fc608d2b200>{number = 5, name = (null)}
UI刷新成功  2020-08-06 06:49:46 +0000  thread: <NSThread: 0x7fc608c0e7e0>{number = 1, name = main}

如你所愿,运行结果依然一致。

使用 group.wait() 改写:

let group = DispatchGroup()
let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)

//异步执行
queue.async(group: group) {
    
    print("开始请求数据 (Date())  thread: (Thread.current)")
    sleep(10)//模拟网络请求
    print("数据请求完成 (Date())  thread: (Thread.current)")
    
    //异步执行
    queue.async(group: group) {
        print("开始处理数据 (Date())  thread: (Thread.current)")
        sleep(5)//模拟数据处理
        print("数据处理完成 (Date())  thread: (Thread.current)")
    }
}

print("开始监听")

//切换到主队列监听,刷新UI
group.notify(queue: DispatchQueue.main) {
    print("UI刷新成功  (Date())  thread: (Thread.current)")
}

group.wait()//阻塞当前线程

print("监听完毕")

运行结果:

开始监听
开始请求数据 2020-08-06 06:53:00 +0000  thread: <NSThread: 0x7fe1ad538580>{number = 4, name = (null)}
数据请求完成 2020-08-06 06:53:10 +0000  thread: <NSThread: 0x7fe1ad538580>{number = 4, name = (null)}
开始处理数据 2020-08-06 06:53:10 +0000  thread: <NSThread: 0x7fe1b8010060>{number = 5, name = (null)}
数据处理完成 2020-08-06 06:53:15 +0000  thread: <NSThread: 0x7fe1b8010060>{number = 5, name = (null)}
监听完毕
UI刷新成功  2020-08-06 06:53:15 +0000  thread: <NSThread: 0x7fe1ad40e7e0>{number = 1, name = main}

可以看到 group.wait() 的确阻塞了当前线程。

进阶篇

DispatchGroup 挂起、恢复

在第7章的例子里,嵌套了三层,还不算多,但是已经可以隐约感受到嵌套地狱了。这一节用队列挂起、恢复重写,解决嵌套问题。以后遇到更多层级的嵌套,可以用同样的方法解决。

let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.apple.request", attributes: .concurrent)
let queue2 = DispatchQueue(label: "com.apple.response", attributes: .concurrent)

queue2.suspend()//队列挂起

//异步执行
queue1.async(group: group) {
    print("开始请求数据 (Date())  thread: (Thread.current)")
    sleep(10)//模拟网络请求
    print("数据请求完成 (Date())  thread: (Thread.current)")
    
    queue2.resume()//网络数据请求完成,恢复队列,进行数据处理
}

//异步执行
queue2.async(group: group) {
    print("开始处理数据 (Date())  thread: (Thread.current)")
    sleep(5)//模拟数据处理
    print("数据处理完成 (Date())  thread: (Thread.current)")
}

print("开始监听")

//切换到主队列监听,刷新UI
group.notify(queue: DispatchQueue.main) {
    print("UI刷新成功  (Date())  thread: (Thread.current)")
}

print("监听完毕")

线程安全

如果有一个变量有可能被多个线程同时读写,结果便不可预期,必须进行特殊处理,来保证线程安全。

通过 barrier 标识设置屏障

自定义队列支持 DispatchWorkItem 设置 flags.barrier ,可以支持 barrier 之前的任务全部执行完毕后,再执行 .barrier 任务,最后再执行 .barrier 之后的任务,这样处理可以保证线程安全。

注:全局队列,flags 设置 .barrier 无效

import Foundation

let item1 = DispatchWorkItem {
    for i in 0...4{
        print("item1 -> (i)  thread: (Thread.current)")
    }
}

let item2 = DispatchWorkItem {
    for i in 0...4{
        print("item2 -> (i)  thread: (Thread.current)")
    }
}

//给item3任务加barrier标识
let item3 = DispatchWorkItem(flags: .barrier) {
    for i in 0...4{
        print("item3 barrier -> (i)  thread: (Thread.current)")
    }
}

let item4 = DispatchWorkItem {
    for i in 0...4{
        print("item4 -> (i)  thread: (Thread.current)")
    }
}

let item5 = DispatchWorkItem {
    for i in 0...4{
        print("item5 -> (i)  thread: (Thread.current)")
    }
}


let queue = DispatchQueue(label: "test", attributes: .concurrent)
queue.async(execute: item1)
queue.async(execute: item2)
queue.async(execute: item3)
queue.async(execute: item4)
queue.async(execute: item5)

运行结果:

item1 -> 0  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item2 -> 0  thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item1 -> 1  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item2 -> 1  thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item1 -> 2  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item2 -> 2  thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item2 -> 3  thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item1 -> 3  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item2 -> 4  thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item1 -> 4  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item3 barrier -> 0  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item3 barrier -> 1  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item3 barrier -> 2  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item3 barrier -> 3  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item3 barrier -> 4  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item4 -> 0  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item5 -> 0  thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item4 -> 1  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item4 -> 2  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item5 -> 1  thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item4 -> 3  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item4 -> 4  thread: <NSThread: 0x7fd6055c07d0>{number = 2, name = (null)}
item5 -> 2  thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item5 -> 3  thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}
item5 -> 4  thread: <NSThread: 0x7fd60560b7f0>{number = 3, name = (null)}

使用 DispatchSemaphore 给线程上锁

DispatchSemaphore 被很多人翻译成信号量,说实话我这辈子第一次听说信号量,信号还有量?什么量?多少量?
吐槽完毕,为了方便理解,在这里我把它临时翻译成红绿灯吧。
DispatchSemaphore 初始化时只有一个参数 value(通行数量),表示还可以通行几辆车(还可以执行几个异步任务)。
DispatchSemaphore 有两个方法:

  • wait()

    执行一次,通行数量减 1,通行数量为 0 时就表示红灯,全都得等着

  • signal()

    执行一次,通行数量加1

举一个 99 乘法表的例子,感受下 DispatchSemaphore:

let semaphore = DispatchSemaphore(value: 1)
let queue = DispatchQueue(label: "concurrent", attributes: .concurrent)
//执行9个异步任务
for i in 1...9 {
    queue.async {
        semaphore.wait()//通行数量减1,此处变为0,红灯,全都得等着
        var str = ""
        for j in 1...9{
            //格式化一下字符串,后面加两个空格。如果只有个位数的,前面补个空格
            let value = i * j
            let tempStr = value <= 9 ? " (value)  " : "(value)  "
            str += tempStr
        }
        print(str)
        semaphore.signal()//通行数量加1,后面可继续通行
    }
}

运行结果:

 1   2   3   4   5   6   7   8   9  
 2   4   6   8  10  12  14  16  18  
 3   6   9  12  15  18  21  24  27  
 4   8  12  16  20  24  28  32  36  
 5  10  15  20  25  30  35  40  45  
 6  12  18  24  30  36  42  48  54  
 7  14  21  28  35  42  49  56  63  
 8  16  24  32  40  48  56  64  72  
 9  18  27  36  45  54  63  72  81

99乘法表显示理想

注释掉 semaphore.wait() 和 semaphore.signal(),多运行几次试试看:

let semaphore = DispatchSemaphore(value: 1)
let queue = DispatchQueue(label: "concurrent", attributes: .concurrent)
//执行9个异步任务
for i in 1...9 {
    queue.async {
        //semaphore.wait()//通行数量减1,此处变为0,红灯,全都得等着
        var str = ""
        for j in 1...9{
            //格式化一下字符串,后面加两个空格。如果只有个位数的,前面补个空格
            let value = i * j
            let tempStr = value <= 9 ? " (value)  " : "(value)  "
            str += tempStr
        }
        print(str)
        //semaphore.signal()//通行数量加1,后面可继续通行
    }
}

运行结果:

 5  10  15  20  25  30  35  40  45  
 4   8  12  16  20  24  28  32  36  
 3   6   9  12  15  18  21  24  27  
 1   2   3   4   5   6   7   8   9  
 8  16  24  32  40  48  56  64  72  
 9  18  27  36  45  54  63  72  81  
 2   4   6   8  10  12  14  16  18  
 6  12  18  24  30  36  42  48  54  
 7  14  21  28  35  42  49  56  63 

99乘法表已经失控

为了更深刻的理解,试试把上面的例 1 中 DispatchSemaphore 初始化时 value 设为 2 或 3,多次运行下程序看看结果,你能感受到通行数量对失控程度的影响。

使用串行队列 + 计算属性,修改变量

import Foundation

let queue = DispatchQueue(label: "test")

var a:Int = 10
var b:Int{
    get{
        queue.sync {
            print("同步读取 thread = (Thread.current)")
            return a
        }
    }
    set{
        queue.sync {
            print("同步写入 thread = (Thread.current)")
            a = newValue
        }
    }
}

b = 30//赋值

print("a = (a)  b = (b) thread = (Thread.current)")

运行结果:

同步写入 thread = <NSThread: 0x7f8018c0e7e0>{number = 1, name = main}
同步读取 thread = <NSThread: 0x7f8018c0e7e0>{number = 1, name = main}
a = 30  b = 30 thread = <NSThread: 0x7f8018c0e7e0>{number = 1, name = main}

尝试修改set为异步写入,思索下结果。

DispatchQoS

DispatchQoS 调度优先级:直译过来就是应用在任务上的服务质量或执行优先级,可以理解为任务的身份、等级。可以用来修饰 DispatchWorkItemDispatchQueue

就像航空公司有身份的客户,在VIP休息室等飞机、坐头等舱、高质量空姐贴心服务等等,最好的服务优先都给你;如果你没有身份、只有身份证,平平安安的到达目的地就可以知足了;如果你连身份证也没有,那就去坐公交车吧。
官网原文:The quality of service, or the execution priority, to apply to tasks.

DispatchQoS 有以下几种类型:

  • userInteractive

    与用户交互相关的任务,要最重视,优先处理,保证界面最流畅

  • userInitiated

    用户主动发起的任务,要比较重视

  • default

    默认任务,正常处理即可

  • utility

    用户没有主动关注的任务

  • background

    不太重要的维护、清理等任务,有空能处理完就行

  • unspecified

    别说身份了,连身份证都没有,能处理就处理,不能处理也无所谓的

DispatchQoS 其实只是一个简单的优先级标识,为何会放在进阶篇里说呢?
因为对于绝大部分开发者来说,没必要设置这个标识,设置了也只是徒增代码复杂度,花里胡哨的技巧用了一大堆,代码量不小,最后到处都是 bug,有意义吗?
还是尽量让代码简单点、少出问题最好,很多书里都讲:代码越少,bug 越少。当有一天你想增强用户体验、提高代码运行效率、优化设备能耗,说明你的应用质量、代码档次都已经很不错了,明显属于进阶水准,这时你应该去试试这个标识了。所以,鄙人认为,DispatchQoS 属于进阶内容。

在 DispatchWorkItem 上添加 DispatchQoS 标识:

import Foundation

let item1 = DispatchWorkItem(qos: .userInteractive) {
    for i in 0...9999{
        print("--item1 -> (i)  thread: (Thread.current)")
    }
}

let item2 = DispatchWorkItem(qos: .unspecified) {
    for i in 0...9999{
        print("item2 -> (i)  thread: (Thread.current)")
    }
}

let queue = DispatchQueue(label: "test1", attributes: .concurrent)
queue.async(execute: item1)
queue.async(execute: item2)

运行结果显示 item1 执行完了,item2 才开始打印 3824。
for 循环次数需要调大一些,否则效果不明显。

在 DispatchQueue 上添加 DispatchQoS 标识:

import Foundation

let item1 = DispatchWorkItem {
    for i in 0...9999{
        print("--item1 -> (i)  thread: (Thread.current)")
    }
}

let item2 = DispatchWorkItem {
    for i in 0...9999{
        print("item2 -> (i)  thread: (Thread.current)")
    }
}

let queue1 = DispatchQueue(label: "test1",qos: .userInteractive, attributes: .concurrent)
let queue2 = DispatchQueue(label: "test2", qos: .unspecified, attributes: .concurrent)
queue1.async(execute: item1)
queue2.async(execute: item2)

我这边运行结果显示 item1 执行完了,item2 才开始打印 3798。
for 循环次数不用太大,效果也可以很明显,您可以自己探索一下。

结束语

要精通 Swift 多线程,还是要多在实践中使用,在使用过程中反复思索、反复优化,这项技术很快就会成为你的拿手好戏。 多线程虽好,但请不要滥用,不要为了炫技去用多线程,毕竟当前的 CPU 性能已经非常高,每秒钟可执行万亿次级别的操作,而屏幕每秒钟仅仅刷新几十、上百次,眨眼的功夫大量的代码就执行完了。在必要的地方再去用多线程吧,代码整洁、问题少、应用稳定可靠才更重要。

注意事项

  • 开发多线程时,养成一个习惯,时刻打印 Thread.current ,看看代码是不是运行在自己预期的线程上。

  • 如果遇到单例之间相互调用,务必注意 DispathQueue 嵌套问题。调用另一个单例时,必要时用 DispatchQueue.main.async{ } 包裹一下,将其切换回主线程,然后在该单例内部的方法中,再进行线程切换。否则线程多层嵌套后,代码将会失控,试图挽救也会无从下手。