美文网首页cocoapodsiOS开发资源iOS大牛修炼之路
Grand Central Dispatch 最全入门手册

Grand Central Dispatch 最全入门手册

作者: Cyandev | 来源:发表于2016-05-16 11:30 被阅读685次

    Grand Central Dispatch 也就是我们常说的 GCD 是苹果为多核心处理器开发的一套异步调度机制。苹果官方文档中是这样介绍它的:

    Grand Central Dispatch (GCD) comprises language features, runtime libraries, and system enhancements that provide systemic, comprehensive improvements to the support for concurrent code execution on multicore hardware in iOS and OS X.

    这里比较重要的一点是它依托了语言特性,绝大部分是 Block 特性,使得欲执行的函数体能很方便地作为参数传递。

    很多刚接触 iOS 开发的朋友可能会认为 GCD 很复杂,事实上这套 API 十分简洁易用,往往使用之后就不会再去用传统的方式处理多线程问题了,熟练使用 GCD 能极大提升我们的开发效率。下面我将从功能的角度逐一讲解它的使用方法。

    <h2>注意,下面的这些函数参数都很明确,我就不一一列举函数的参数是什么了。</h2>

    Queue

    GCD 的异步核心是列队,系统会有自己的主列队和全局列队,任何在主列队进行的任务都会阻塞 UI 事件,我们只要了解这一点就可以了。

    下面是获取和创建列队的函数:

    • dispatch_get_main_queue: 获取主列队。
    • dispatch_get_global_queue: 获取全局列队,其中第一个参数为优先级,可选的优先级有以下几个:
      • QOS_CLASS_USER_INTERACTIVE
      • QOS_CLASS_USER_INITIATED
      • QOS_CLASS_DEFAULT
      • QOS_CLASS_UTILITY
      • QOS_CLASS_BACKGROUND
        从上到下优先级依次降低,该函数的第二个参数为系统保留,每次都传入 0 即可。通常我们第一个参数也传入 0 来获取默认的全局列队。
    • dispatch_queue_create: 创建一个列队,第一个参数为标识符,通常用倒过来写的域名 (eg. com.apple.myqueue) ,第二个参数为列队类型,有下列类型可选:
      • DISPATCH_QUEUE_SERIAL
      • DISPATCH_QUEUE_CONCURRENT
        顾名思义,一个是串行的,一个是并行的。如果传入 NULL则表示串行列队。

    有了列队,我们就可以向其中分发任务了,下面是分发任务时需要的函数:

    • dispatch_async: 将执行体放入列队然后立即返回,如果列队是主列队,那么就相当于延后执行代码。
    • dispatch_sync: 将执行体放入列队并执行,等待执行完毕再返回。在主线程中,列队坚决不能是主列队,不然主线程被挂起来等待任务执行结束,然而这个任务就是要在主线程执行,那么这个任务永远不会执行,主线程也永远不会被唤醒。同理列队也不能是调用 dispatch_sync 的当前列队,同样会产生死锁。
    • dispatch_apply: 迭代执行,第一个参数是迭代次数,第二个参数是目标列队,第三个参数是欲执行的 block,block 的参数就是当前迭代的下标。注意,这个函数将会等待所有执行体完毕后才返回。适当情况下可以用这个函数来代替 for-loop 以提高性能。

    通常,我们在 Global Queue 中执行一些耗时操作,然后在 Main Queue 中将结果更新到 UI。

    Time

    GCD 有几个比较重要的函数与时间相关:

    • dispatch_time: 创建一个时间对象,第一个参数是参考时间,第二个参数是时间增量,那么最终返回的时间就是 (参考时间 + 增量),通常使用方法有下面几种:
      • dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_MSEC * 100)): 当前时间后 100 毫秒
      • dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC * 3)): 当前时间后 3 秒
        其中 NSEC_PER_MSECNSEC_PER_SEC 是时间计量单位。
    • dispatch_after: 在指定时间后执行,这个函数将立即返回,等到指定时间后才会在指定的列队中执行指定任务。

    Group

    如果需要批量执行一批任务,并且想在所有这批任务结束后得到通知,这时就需要用到 Group 了。

    • dispatch_group_create: 创建一个组,无任何参数。
    • dispatch_group_async: 同 dispatch_async ,只不过第一个参数前多了一个 group ,即将任务归为这一组。
    • dispatch_group_enter: 增加组的计数,此时你不需要提交执行体,等待 dispatch_group_leave 被调用后则表示一个任务完成,可嵌套,总之保证两者调用次数平衡即可。下面是一个例子:
    let group = dispatch_group_create()
    
    dispatch_group_enter(group)
    dispatch_group_enter(group)
    dispatch_group_leave(group)
    dispatch_group_leave(group)
    
    • dispatch_group_notify: 在该组任务都完成后在指定列队中调用一个回调函数。
    • dispatch_group_wait: 阻塞线程直到该组任务都完成,或指定时间超时后返回。

    Barrier

    这个词,有意思。


    看翻译感觉很难理解,所以我用通俗的语言表述一下。通常我们会遇到这样一个问题:一个线程需要访问资源 A,而另外一个线程需要修改资源 B,这时就会有竞态问题。我们坚决不能让这个情况出现,这里我们就可以用 Barrier 的这个函数:

    dispatch_barrier_async

    执行这个函数后,执行体将会被放到列队中并等待,直到整个列队中没有在执行的任务,这时这个执行体才会被执行,并且在它完成之前不会有其他任务会同时进行。简单说就是这个执行体必须自己一个人执行,不得有人和它一同执行。

    说起来还是好抽象,上代码:

    import Foundation
    
    class Counter {
        
        var counter: Int = 0
        let queue = dispatch_queue_create("com.example.barrierqueue", nil)
        
        func get() -> Int {
            var result: Int!
            dispatch_sync(queue) { 
                result = self.counter
            }
            return result
        }
        
        func enter() {
            adjustBy(1)
        }
        
        func leave() {
            adjustBy(-1)
        }
        
        private func adjustBy(by: Int) {
            dispatch_barrier_async(queue) { 
                self.counter += by
            }
        }
        
    }
    

    我们可以看到,访问一个变量可以是同时进行的,因为他们不会修改这个变量。然而进行修改时必须一个一个来,这里我们就可以用 barrier 挡一下,等修改完了再干别的。

    Semaphore

    信号量是一个控制资源访问数量的方法,例如一个资源只能被 5 个人同时访问,那么我们就可以用信号量记录访问数,并阻挡超出数量的人进行的访问。主要函数有下面几个:

    • dispatch_semaphore_create: 传入一个整型作为最大访问数量,创建并返回这个信号量类型。
    • dispatch_semaphore_wait: 使用资源前调用它,它会减少计数器,如果计数器小于零,这个函数将会阻塞。
    • dispatch_semaphore_signal: 使用资源后调用它,它会增加计数器,如果计数器大于等于零,那些等待使用资源的线程会被唤醒。

    其实很多情况我们都用信号量来将一些无法修改的异步函数变成同步函数,下面是一个例子:

    func fetchURLSync(URL: NSURL) -> NSData? {
        let sem = dispatch_semaphore_create(0)
        
        var _data: NSData?
        let task = NSURLSession.sharedSession().dataTaskWithURL(URL) { (data, response, error) in
            _data = data
            
            dispatch_semaphore_signal(sem)
        }
        
        task.resume()
        
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER)
        
        return _data
    }
    

    Source

    用过 CFRunloop / NSRunloop 的朋友应该不陌生这个词,GCD也有 Source 的概念,怎么理解呢,Source 就像一个信号发射器,有信号发出就会有响应的处理。比较常见的信号类型有 Timer文件读写进程UNIX SignalMach Port内存压力。但是在 iOS 开发中,我们也就是用用 Timer 了,下面直接上一个例子:

    func createAndStartTimer(interval: UInt64, leeway: UInt64, queue: dispatch_queue_t, block: () -> Void) -> dispatch_source_t? {
        let timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue)
        
        if timer != nil {
            dispatch_source_set_timer(timer, dispatch_walltime(nil, 0), interval, leeway)
            dispatch_source_set_event_handler(timer, block)
            dispatch_resume(timer)
        }
        
        return timer
    }
    
    func stopTimer(timer: dispatch_source_t) {
        dispatch_source_cancel(timer)
    }
    

    上面代码拿来用就好,leeway 是精准度的意思,精准度越高资源消耗越大。

    洋洋洒洒写了这么多,应该算覆盖得比较全了,就酱。

    相关文章

      网友评论

      • jmstack:关于dispatch_semaphore_wait苹果的文档是这样说的Decrement the counting semaphore. If the resulting value is less than zero, this function waits in FIFO order for a signal to occur before returning. 和你说的刚好相反.
        Cyandev:@jmstack 感谢勘误

      本文标题:Grand Central Dispatch 最全入门手册

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