14

Swift多线程编程总结 - 简书

 4 years ago
source link: https://www.jianshu.com/p/3a8ea5505ef6?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
0.1682018.12.19 16:01:18字数 2,437阅读 1,832

在开始多线程之前,我们先来了解几个比较容易混淆的概念。

线程与进程

线程与进程之间的关系,拿公司举例,进程相当于部门,线程相当于部门职员。即进程内可以有一个或多个线程。

并发和并行

并发指的是多个任务交替占用CPU,并行指的是多个CPU同时执行多个任务。好比火车站买票,并发指的是一个窗口有多人排队买票,而并行指的是多个窗口有多人排队买票。

同步和异步

同步指在执行一个函数时,如果这个函数没有执行完毕,那么下一个函数便不能执行。异步指在执行一个函数时,不必等到这个函数执行完毕,便可开始执行下一个函数。

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也有waitnotify方法,和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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK