美文网首页Swift
Swift多线程编程总结

Swift多线程编程总结

作者: 我的大好时光 | 来源:发表于2018-12-19 16:01 被阅读26次

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

    概念

    线程与进程

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

    并发和并行

    并发指的是多个任务交替占用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也有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
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    相关文章

      网友评论

        本文标题:Swift多线程编程总结

        本文链接:https://www.haomeiwen.com/subject/uwmjkqtx.html