一天精通iOS Swift多线程(GCD)

15,488 阅读24分钟

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

本文前半部分,我会尽可能精简话语,降低入门门槛,随着理解的深入,后面我会循序渐进地讲详细一些。

第一部分:基础篇

1. 串行、并行、同步、异步

  • 串行:在本文中指串行队列,多个任务放在串行队列里执行,只能按顺序依次运行,前一个运行完成,下一个才能开始运行;前一个没运行完,后一个只能排队等着。以此类推,直到所有任务都运行完成。
  • 并行:在本文中指并行队列,多个任务放在并行队列里执行,可以同时运行。
  • 同步:在本文中指同步执行任务,是在一个线程里按顺序执行多项任务,执行结束的顺序是固定的、和任务的执行顺序相同。总耗时是所有任务耗时之和。
  • 异步:在本文中指异步执行任务,也是按顺序执行多项任务,但是是放在多个线程里同时运行,执行结束的顺序是随机的、不可预估的。总耗时大约是耗时最长的那项任务所消耗的时间。

2. 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")
}

3. 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

3.1 Main queue(主队列,串行)

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

let mainQueue = DispatchQueue.main

3.2 Global queue(全局队列,并行)

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

let globalQueue = DispatchQueue.global()

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

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

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

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

4. DispatchGroup简介

调度组:一个小组,你可以把多项任务放到一个组里,方便进行统一管理(直译过来并不好理解)。
官网原文:A group of tasks that you monitor as a single unit.

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

  • notify():调度组里的所有任务执行完毕,会在此收到通知,不会阻塞当前线程。
  • wait():一直等待,直到调度组里所有任务都执行完毕或等待超时,阻塞当前线程。

第二部分:实战篇

5. 使用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)")
    }
}

5.1 异步执行

//主队列追加异步任务,按顺序打印
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)

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

5.2 同步执行

//主队列追加同步任务,会引起死锁
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)

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

5.3 同步异步混合执行

//主队列同步异步混合,会引起死锁
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)

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

6. 死锁分析

6.1 主队列死锁

上文提到了主队列不能混入同步任务,否则会引起死锁,为何呢?因为主队列是串行队列,并且仅能运行在主线程上,它无法去创建新的线程,也就意味着所有的代码都必须在只能在一个线程上运行。
正常情况下,主队列上存在源源不断的异步任务(比如用来不断刷新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)

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

6.2 其他队列死锁

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

  • 自定义串行队列嵌套同步任务,会引起死锁
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)")
    }
}

6.3 死锁总结

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

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执行完了,逻辑上已经陷入死循环,两者相互等待,导致死锁。所以,串行队列不能嵌套同步任务,否则会引起死锁。

7. DispatchQueue切换

7.1 背景介绍

这一章来模拟网络请求:在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,因为上面说到了:主线程运行长耗时任务都会导致界面严重卡顿。

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

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

7.2 网络请求实例

现在讲讲使用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队列切换像俄罗斯套娃一样,一层一层的嵌套就行,等嵌套出问题了,去第6章死锁分析寻找原因进行修改即可。

8. 使用DispatchGroup

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

DispatchGroup常用的方法:

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

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

8.1 使用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}

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

8.2 精简代码,直接在主队列监听通知、刷新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}

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

8.3 使用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()的确阻塞了当前线程。

第三部分:进阶篇

9. 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("监听完毕")

10. 线程安全

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

10.1 通过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)}

10.2 使用DispatchSemaphore给线程上锁

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

  • wait():执行一次,通行数量减1,通行数量为0时就表示红灯,全都得等着
  • signal():执行一次,通行数量加1

10.2.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乘法表显示理想

10.2.2 注释掉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,多次运行下程序看看结果,你能感受到通行数量对失控程度的影响。

10.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为异步写入,思索下结果。

11. DispatchQoS

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

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

DispatchQoS有以下几种类型:

  • userInteractive: 与用户交互相关的任务,要最重视,优先处理,保证界面最流畅
  • userInitiated: 用户主动发起的任务,要比较重视
  • default: 默认任务,正常处理即可
  • utility: 用户没有主动关注的任务
  • background: 不太重要的维护、清理等任务,有空能处理完就行
  • unspecified: 别说身份了,连身份证都没有,能处理就处理,不能处理也无所谓的

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

11.1 在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循环次数需要调大一些,否则效果不明显。

11.2 在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{ }包裹一下,将其切换回主线程,然后在该单例内部的方法中,再进行线程切换。否则线程多层嵌套后,代码将会失控,试图挽救也会无从下手。

作者简介

我是朱成绩,一名开发者,在上海工作,专注于iOS、Swift、SwiftUI方向,TypeScript也在使用。