Swift多线程编程总结
在开始多线程之前,我们先来了解几个比较容易混淆的概念。
概念
线程与进程
线程与进程之间的关系,拿公司举例,进程相当于部门,线程相当于部门职员。即进程内可以有一个或多个线程。
并发和并行
并发指的是多个任务交替占用CPU,并行指的是多个CPU同时执行多个任务。好比火车站买票,并发指的是一个窗口有多人排队买票,而并行指的是多个窗口有多人排队买票。
同步和异步
同步指在执行一个函数时,如果这个函数没有执行完毕,那么下一个函数便不能执行。异步指在执行一个函数时,不必等到这个函数执行完毕,便可开始执行下一个函数。
GCD
Swift3之后,GCD的Api有很大的调整,从原来的C语言风格的函数调用,变为面向对象的封装,使用起来更加舒服,灵活性更高。
同步
let queue = DispatchQueue(label: "com.ffib.blog")
queue.sync {
for i in 0..<5 {
print(i)
}
}
for i in 10..<15 {
print(i)
}
output:
0
1
2
3
4
10
11
12
13
14
复制代码
从结果可以看出队列同步操作时,当程序在进行队列任务时,主线程的操作并不会被执行,这是由于当程序在执行同步操作时,会阻塞线程,所以需要等待队列任务执行完毕,程序才可以继续执行。
异步
let queue = DispatchQueue(label: "com.ffib.blog")
queue.async {
for i in 0..<5 {
print(i)
}
}
for i in 10..<15 {
print(i)
}
output:
10
0
11
1
12
2
13
3
14
4
复制代码
从结果可以看出队列异步操作时,当程序在执行队列任务时,不必等待队列任务开始执行,便可执行主线程的操作。与同步执行相比,异步队列并不会阻塞主线程,当主线程空闲时,便可执行别的任务。
QoS 优先级
在实际开发中,我们需要对任务分类,比如UI的显示和交互操作等,属于优先级比较高的,有些不着急操作的,比如缓存操作、用户习惯收集等,相对来说优先级比较低。
在GCD中,我们使用队列和优先级划分任务,以达到更好的用户体验,选择合适的优先级,可以更好的分配CPU的资源。
GCD内采用DispatchQoS结构体,如果没有指定QoS,会使用default
。 以下等级由高到低。
public struct DispatchQoS : Equatable {
public static let userInteractive: DispatchQoS //用户交互级别,需要在极快时间内完成的,例如UI的显示
public static let userInitiated: DispatchQoS //用户发起,需要在很快时间内完成的,例如用户的点击事件、以及用户的手势
。
public static let `default`: DispatchQoS //系统默认的优先级,
public static let utility: DispatchQoS //实用级别,不需要很快完成的任务
public static let background: DispatchQoS //用户无法感知,比较耗时的一些操作
public static let unspecified: DispatchQoS
}
复制代码
以下通过两个例子来具体看一下优先级的使用。
相同优先级
let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)
queue1.async {
for i in 5..<10 {
print(i)
}
}
queue2.async {
for i in 0..<5 {
print(i)
}
}
output:
0
5
1
6
2
7
3
8
4
9
复制代码
从结果可见,优先级相同时,两个队列是交替执行的。
不同优先级
let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .default)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)
queue1.async {
for i in 0..<5 {
print(i)
}
}
queue2.async {
for i in 5..<10 {
print(i)
}
}
output:
0
5
1
2
3
4
6
7
8
9
复制代码
从结果可见,交替输出,CPU会把更多的资源优先分配给优先级高的队列,等到CPU空闲之后才会分配资源给优先级低的队列。
主队列默认使用拥有最高优先级,即userInteractive
,所以慎用这一优先级,否则极有可能会影响用户体验。
一些不需要用户感知的操作,例如缓存等,使用utility
即可
串行队列
在创建队列时,不指定队列类型时,默认为串行队列。
let queue = DispatchQueue(label: "com.ffib.blog.initiallyInactive.queue", qos: .utility)
queue.async {
for i in 0..<5 {
print(i)
}
}
queue.async {
for i in 5..<10 {
print(i)
}
}
queue.async {
for i in 10..<15 {
print(i)
}
}
output:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码
从结果可见队列执行结果,是按任务添加的顺序,依次执行。
并行队列
let queue = DispatchQueue(label: "com.ffib.blog.concurrent.queue", qos: .utility, attributes: .concurrent)
queue.async {
for i in 0..<5 {
print(i)
}
}
queue.async {
for i in 5..<10 {
print(i)
}
}
queue.async {
for i in 10..<15 {
print(i)
}
}
output:
5
0
10
1
2
3
11
4
6
12
7
13
8
14
9
复制代码
从结果可见,所有任务是以并行的状态执行的。另外在设置attributes
参数时,参数还有另一个枚举值initiallyInactive
,表示的任务不会自动执行,需要程序员去手动触发。如果不设置,默认是添加完任务后,自动执行。
let queue = DispatchQueue(label: "com.ffib.blog.concurrent.queue", qos: .utility,
attributes: .initiallyInactive)
queue.async {
for i in 0..<5 {
print(i)
}
}
queue.async {
for i in 5..<10 {
print(i)
}
}
queue.async {
for i in 10..<15 {
print(i)
}
}
//需要调用activate,激活队列。
queue.activate()
output:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码
从结果可见,只是把自动执行变为手动触发,执行结果没变,添加这一属性带来了,更多的灵活性,可以自由的决定执行的时机。
再来看看并行队列如何设置这一枚举值。
let queue = DispatchQueue(label: "com.ffib.blog.concurrent.queue", qos: .utility, attributes:
[.concurrent, .initiallyInactive])
queue.async {
for i in 0..<5 {
print(i)
}
}
queue.async {
for i in 5..<10 {
print(i)
}
}
queue.async {
for i in 10..<15 {
print(i)
}
}
queue.activate()
output:
10
0
5
11
1
6
12
2
7
13
3
8
14
4
9
复制代码
延时执行
GCD提供了任务延时执行的方法,通过对已创建的队列,调用延时任务的函数即可。其中时间以DispatchTimeInterval
设置,GCD内跟时间参数有关系的参数都是通过这一枚举来设置。
public enum DispatchTimeInterval : Equatable {
case seconds(Int) //秒
case milliseconds(Int) //毫秒
case microseconds(Int) //微妙
case nanoseconds(Int) //纳秒
case never
}
复制代码
在设置调用函数时,asyncAfter
有两个及其相同的方法,不同的地方在于参数名有所不同,参照Stack Overflow的解释。
wallDeadline 和 deadline,当系统睡眠后,wallDeadline会继续,但是deadline会被挂起。例如:设置参数为60分钟,当系统睡眠50分钟,wallDeadline会在系统醒来之后10分钟执行,而deadline会在系统醒来之后60分钟执行。
let queue = DispatchQueue(label: "com.ffib.blog.after.queue")
let time = DispatchTimeInterval.seconds(5)
queue.asyncAfter(wallDeadline: .now() + time) {
print("wall dead line done")
}
queue.asyncAfter(deadline: .now() + time) {
print("dead line done")
}
复制代码
DispatchGroup
如果想等到所有的队列的任务执行完毕再进行某些操作时,可以使用DispatchGroup
来完成。
let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)
queue1.async(group: group) {
for i in 0..<10 {
print(i)
}
}
queue2.async(group: group) {
for i in 10..<20 {
print(i)
}
}
//group内所有线程的任务执行完毕
group.notify(queue: DispatchQueue.main) {
print("done")
}
output:
5
0
6
1
7
2
8
3
9
4
done
复制代码
如果想等待某一队列先执行完毕再执行其他队列可以使用wait
let group = DispatchGroup()
let queue1 = DispatchQueue(label: "com.ffib.blog.queue1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.queue2", qos: .utility)
queue1.async(group: group) {
for i in 0..<10 {
print(i)
}
}
queue2.async(group: group) {
for i in 10..<20 {
print(i)
}
}
group.wait()
//group内所有线程的任务执行完毕
group.notify(queue: DispatchQueue.main) {
print("done")
}
output:
0
1
2
3
4
5
6
7
8
9
done
复制代码
为防止队列执行任务时出现阻塞,导致线程锁死,可以设置超时时间。
group.wait(timeout: <#T##DispatchTime#>)
group.wait(wallTimeout: <#T##DispatchWallTime#>)
复制代码
DispatchWorkItem
Swift3新增的api,可以通过此api设置队列执行的任务。先看看简单应用吧。通过DispatchWorkItem
初始化闭包。
let workItem = DispatchWorkItem {
for i in 0..<10 {
print(i)
}
}
复制代码
调用一共分两种情况,第一种是通过调用perform()
,自动响应闭包。
DispatchQueue.global().async {
workItem.perform()
}
复制代码
第二种是作为参数传给async
方法。
DispatchQueue.global().async(execute: workItem)
复制代码
接下来我们来看看DispatchWorkItem
的内部都有些什么方法和属性。
init(qos: DispatchQoS = default, flags: DispatchWorkItemFlags = default,
block: @escaping () -> Void)
复制代码
从初始化方法开始,DispatchWorkItem
也可以设置优先级,另外还有个参数DispatchWorkItemFlags
,来看看DispatchWorkItemFlags
的内部组成。
public struct DispatchWorkItemFlags : OptionSet, RawRepresentable {
public static let barrier: DispatchWorkItemFlags
public static let detached: DispatchWorkItemFlags
public static let assignCurrentContext: DispatchWorkItemFlags
public static let noQoS: DispatchWorkItemFlags
public static let inheritQoS: DispatchWorkItemFlags
public static let enforceQoS: DispatchWorkItemFlags
}
复制代码
DispatchWorkItemFlags
主要分为两部分:
- 覆盖
- noQoS 没有优先级
- inheritQoS 继承Queue的优先级
- enforceQoS 覆盖Queue的优先级
- 执行情况
- barrier
- detached
- assignCurrentContext
执行情况会在下文会具体描述,先在这留个坑。
先来看看设置优先级,会对任务执行有什么影响。
let queue1 = DispatchQueue(label: "com.ffib.blog.workItem1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.workItem2", qos: .userInitiated)
let workItem1 = DispatchWorkItem(qos: .userInitiated) {
for i in 0..<5 {
print(i)
}
}
let workItem2 = DispatchWorkItem(qos: .utility) {
for i in 5..<10 {
print(i)
}
}
queue1.async(execute: workItem1)
queue2.async(execute: workItem2)
output:
5
0
6
7
8
9
1
2
3
4
复制代码
由结果可见即使设置了DispatchWorkItem
仅仅只设置了优先级并不会对任务执行顺序有任何影响。
接下来,再来设置DispatchWorkItemFlags
试试
let queue1 = DispatchQueue(label: "com.ffib.blog.workItem1", qos: .utility)
let queue2 = DispatchQueue(label: "com.ffib.blog.workItem2", qos: .userInitiated)
let workItem1 = DispatchWorkItem(qos: .userInitiated, flags: .enforceQoS) {
for i in 0..<5 {
print(i)
}
}
let workItem2 = DispatchWorkItem {
for i in 5..<10 {
print(i)
}
}
queue1.async(execute: workItem1)
queue2.async(execute: workItem2)
output:
5
0
6
1
7
2
8
3
9
4
复制代码
设置enforceQoS
,使优先级强制覆盖queue的优先级,所以两个队列呈交替执行状态,变为同一优先级。
DispatchWorkItem
也有wait
和notify
方法,和DispatchGroup
用法相同。
DispatchSemaphore
如果你想同步执行一个异步队列任务,可以使用信号量。
wait()
会使信号量减一,如果信号量大于1则会返回.success
,否则返回timeout
(超时),也可以设置超时时间。
func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult
func wait(timeout: DispatchTime) -> DispatchTimeoutResult
复制代码
signal()
会使信号量加一,返回当前信号量。
func signal() -> Int
复制代码
下面通过实例来看看具体的使用。
先看看不使用信号量时,在文件异步写入会发生什么。
//初始化信号量为1
let semaphore = DispatchSemaphore(value: 1)
let queue = DispatchQueue(label: "com.ffib.blog.queue", qos: .utility, attributes: .concurrent)
let fileManager = FileManager.default
let path = NSHomeDirectory() + "/test.txt"
print(path)
fileManager.createFile(atPath: path, contents: nil, attributes: nil)
//循环写入,预期结果为test4
for i in 0..<5 {
queue.async {
do {
try "test\(i)".write(toFile: path, atomically: true, encoding: String.Encoding.utf8)
}catch {
print(error)
}
semaphore.signal()
}
}
}
复制代码
<figure>[图片上传中...(image-135c2a-1545206459030-1)]
<figcaption></figcaption>
</figure>
发现写入的结果根本不是我们想要的。此时再使用信号量试试。
let semaphore = DispatchSemaphore(value: 1)
let queue = DispatchQueue(label: "com.ffib.blog.queue", qos: .utility, attributes: .concurrent)
let fileManager = FileManager.default
let path = NSHomeDirectory() + "/test.txt"
print(path)
fileManager.createFile(atPath: path, contents: nil, attributes: nil)
for i in 0..<5 {
//.distantFuture代表永远
if semaphore.wait(wallTimeout: .distantFuture) == .success {
queue.async {
do {
print(i)
try "test\(i)".write(toFile: path, atomically: true, encoding: String.Encoding.utf8)
}catch {
print(error)
}
semaphore.signal()
}
}
}
复制代码
<figure>[图片上传中...(image-f59274-1545206459030-0)]
<figcaption></figcaption>
</figure>
写入的结果符合预期效果,
我们来看下for
循环里都发生了什么。第一遍循环遇到wait
时,此时信号量为1,大于0,所以if
判断为true
,进行写入操作;当第二遍循环遇到wait
时,发现信号量为0,此时就会锁死线程,直到上一遍循环的写入操作完成,调用signal()
方法,信号量加一,才会执行写入操作,循环以上操作。好奇的同学,可以加上sleep(1)
,然后打开文件夹,会发现test.txt
文件从test1
不断加1变为test4
。(ps:写入文件的方式略显粗糙,不过这不是本文讨论的重点,仅用以测试DispatchSemaphore
)
DispatchSemaphore
还有另外一个用法,可以限制队列的最大并发量,通过前面所说的wait()
信号量减一,signal()
信号量加一,来完成此操作,正如上文所述例子,其实达到的效果就是最大并发量为一。
如果使用过NSOperationQueue
的同学,应该知道maxConcurrentOperationCount
,效果是类似的。
DispatchWorkItemFlags
前面留了个DispatchWorkItemFlags
的坑,现在来具体看看。
barrier
可以理解为隔离,还是以文件读写为例,在读取文件时,可以异步访问,但是如果突然出现了异步写入操作,我们想要达到的效果是在进行写入操作的时候,使读取操作暂停,直到写入操作结束,再继续进行读取操作,以保证读取操作获取的是文件的最新内容。
以上文中的test.txt
文件为例,预期结果是:在写入操作之前,读取到的内容是test4
;在写入操作之后,读取到的内容是done
(即写入的内容)。
先看看不使用barrier
的结果。
let queue = DispatchQueue(label: "com.ffib.blog.queue", qos: .utility, attributes: .concurrent)
let path = NSHomeDirectory() + "/test.txt"
print(path)
let readWorkItem = DispatchWorkItem {
do {
let str = try String(contentsOfFile: path, encoding: .utf8)
print(str)
}catch {
print(error)
}
sleep(1)
}
let writeWorkItem = DispatchWorkItem(flags: []) {
do {
try "done".write(toFile: path, atomically: true, encoding: String.Encoding.utf8)
print("write")
}catch {
print(error)
}
sleep(1)
}
for _ in 0..<3 {
queue.async(execute: readWorkItem)
}
queue.async(execute: writeWorkItem)
for _ in 0..<3 {
queue.async(execute: readWorkItem)
}
output:
test4
test4
test4
test4
test4
test4
write
复制代码
结果不是我们想要的。再来看看加了barrier
之后的效果。
let queue = DispatchQueue(label: "com.ffib.blog.queue", qos: .utility, attributes: .concurrent)
let path = NSHomeDirectory() + "/test.txt"
print(path)
let readWorkItem = DispatchWorkItem {
do {
let str = try String(contentsOfFile: path, encoding: .utf8)
print(str)
}catch {
print(error)
}
}
let writeWorkItem = DispatchWorkItem(flags: .barrier) {
do {
try "done".write(toFile: path, atomically: true, encoding: String.Encoding.utf8)
print("write")
}catch {
print(error)
}
}
for _ in 0..<3 {
queue.async(execute: readWorkItem)
}
queue.async(execute: writeWorkItem)
for _ in 0..<3 {
queue.async(execute: readWorkItem)
}
output:
test4
test4
test4
write
done
done
done
复制代码
结果符合预期的想法,barrier
主要用于读写隔离,以保证写入的时候,不被读取。
作者:FFIB
链接:https://juejin.im/post/5a4c542b6fb9a045211f17ac
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。