GCD

作者: 阿斯兰iOS | 来源:发表于2019-06-15 19:11 被阅读0次

    官网: https://developer.apple.com/reference/dispatch?language=objc#symbols

    文章是对文档和 API 的学习总结,内容包括 GCD 的概念和基本使用、任务组 dispatch_group、栅栏 dispatch_barrier、信号量 dispatch_semaphore_t、时间 dispatch_time、以及 dispatch_block_t 的简介。

    GCD 常用的一个重要功能是,等待所有异步操作完成后(比如多个接口拿数据),追加执行一个同步操作,具体可以看第 2 部分的内容。


    GCD 全称是 Grand Central Dispatch。

    GCD 对象在 ARC 下会自动管理内存,不需要使用 dispatch_retain 和 dispatch_release。GCD 对象其实是 NSObject 对象。

    几个重要的概念:派发,GCD 的同步、异步,串行、并行队列

    • 派发是指把 block 提交到队列执行。
    • 同步就是阻塞当前线程直到任务完成,异步就是不阻塞当前线程。
    • 串行队列是指一个任务完成后,才能开始下一个。
    • 并行队列也是按 FIFO 的顺序开始任务,但不需要等待上一个任务完成后,才能开始下一个。它可以随意开始多个任务。

    串行队列可能造成死锁,比如在一个任务 A 里面,给自己同步派发一个任务 B。因为是同步派发,所以会阻塞任务 A 的执行,等待任务 B 完成。但串行队列只能开始一个任务,所以任务 B 也在等待任务 A 的完成,于是造成了死锁。

    有人会问,同步、异步 + 串行、并行队列,比如同步派发到并行队列,是否会开辟新的线程?
    使用 GCD 不就是为了不用管理线程吗?系统创建和维护一个线程池,理论上每个队列都有自己的线程,是否开辟线程由系统决定。只需要知道,是否会阻塞当前线程,任务是否会开始,是否会死锁,就可以了。


    1、管理队列

    先看队列的定义:

    // 队列的定义
    typedef NSObject<OS_dispatch_queue> *dispatch_queue_t;
    

    串行队列一次只执行一个 block,完成后再执行下一个。
    并行队列可以同时执行多个 block,开始执行的顺序按照 FIFO,不用等前一个 block 完成才开始下一个。
    系统维护一个线程池,理论上队列都有自己的线程。
    队列的 block 会被强引用,直到执行完。

    1.1 创建队列 dispatch_queue_create

    // label:队列标识,用于调试工具,比如 Instruments, crash reports。
    // label:域名的倒序,比如 com.example.myqueue,可以是 NULL。
    // attr:传 NULL 或 DISPATCH_QUEUE_SERIAL 创建串行队列。
    // attr:传 DISPATCH_QUEUE_CONCURRENT 创建并行队列。
    dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
    
    // 并行队列
    dispatch_queue_t queue = dispatch_queue_create("com.my.queue", DISPATCH_QUEUE_CONCURRENT);
    
    // 串行队列
    dispatch_queue_t queue = dispatch_queue_create("com.my.queue", NULL); 
    

    ARC 会自动管理内存,MRC 需要调用 dispatch_release 释放。
    添加到队列的 block 会对 queue 强引用,因此所有 block 执行后,队列才会释放。
    If your app isn’t using ARC, you should call dispatch_release on a dispatch queue when it’s no longer needed. Any pending blocks submitted to a queue hold a reference to that queue, so the queue is not deallocated until all pending blocks have completed.

    1.2 主队列 dispatch_get_main_queue

    // 获取主队列
    dispatch_queue_main_t dispatch_get_main_queue(void);
    
    dispatch_queue_main_t main = dispatch_get_main_queue();
    

    dispatch_suspend、dispatch_resume、dispatch_set_context,对主队列无效。

    1.3 全局队列 dispatch_get_global_queue

    // 获取全局队列,是一个并发队列。
    // flags:传 0
    // identifier:优先级,用于竞争系统资源。
    dispatch_queue_global_t dispatch_get_global_queue(long identifier, unsigned long flags);
    
    
    // 获取一个默认优先级的全局队列
    long identifier = 0;
    identifier = QOS_CLASS_DEFAULT;
    identifier = DISPATCH_QUEUE_PRIORITY_DEFAULT;
    dispatch_queue_t global = dispatch_get_global_queue(identifier, 0);
    

    dispatch_suspend、dispatch_resume、dispatch_set_context,对全局队列无效。

    关于参数 identifier,可以是 qos_class_t 或 dispatch_queue_priority_t,比如 QOS_CLASS_DEFAULT 和 DISPATCH_QUEUE_PRIORITY_DEFAULT 是等级的。
    但是 qos_class_t 没有文档注释,可以看 NSQualityOfService 的定义,它们也是对应的,下文有介绍。

     @param identifier
     A quality of service class defined in qos_class_t or a priority defined in
     dispatch_queue_priority_t.
    
     It is recommended to use quality of service class values to identify the
     well-known global concurrent queues:
      - QOS_CLASS_USER_INTERACTIVE
      - QOS_CLASS_USER_INITIATED
      - QOS_CLASS_DEFAULT
      - QOS_CLASS_UTILITY
      - QOS_CLASS_BACKGROUND
    
     The global concurrent queues may still be identified by their priority,
     which map to the following QOS classes:
      - DISPATCH_QUEUE_PRIORITY_HIGH:         QOS_CLASS_USER_INITIATED
      - DISPATCH_QUEUE_PRIORITY_DEFAULT:      QOS_CLASS_DEFAULT
      - DISPATCH_QUEUE_PRIORITY_LOW:          QOS_CLASS_UTILITY
      - DISPATCH_QUEUE_PRIORITY_BACKGROUND:   QOS_CLASS_BACKGROUND
    
    (可见 QOS_CLASS_USER_INTERACTIVE 比 DISPATCH_QUEUE_PRIORITY_HIGH 还高。)
    
    
    // 优先级 NSQualityOfService 的定义
    // 和 qos_class_t 是对应的
    
    typedef NS_ENUM(NSInteger, NSQualityOfService) {
        NSQualityOfServiceUserInteractive = 0x21,
        NSQualityOfServiceUserInitiated = 0x19,
        NSQualityOfServiceUtility = 0x11,
        NSQualityOfServiceBackground = 0x09,
        NSQualityOfServiceDefault = -1
    } API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
    
    // 用于 UI 交互,比如处理控件事件和屏幕绘制
    NSQualityOfServiceUserInteractive
    
    // 用于用户发起的请求,而且结果必须立即反馈给用户继续交互的任务。
    // 比如用户点击列表触发的数据加载。
    NSQualityOfServiceUserInitiated
    
    // 用于用户不需要立即得到结果的任务。
    // 比如大量文件操作,media 导入等。
    NSQualityOfServiceUtility
    
    // 用于用户不可见、无需用户察觉的任务。
    // 比如数据备份、内容预加载。
    NSQualityOfServiceBackground
    
    // 表示没有定义优先级。
    NSQualityOfServiceDefault
    

    1.4 异步派发 dispatch_async

    // 不会阻塞当前线程
    // 内部会调用 Block_copy 、 Block_release。
    void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
    

    1.5 同步派发 dispatch_sync

    // 会阻塞当前线程,block 执行完才返回。
    // 在当前队列,同步派发到当前队列,会死锁。
    // 比如在主线程,同步派发到主队列。
    // 为了优化,一般会在当前线程执行 block。
    // no retain or Block_copy is performed on the block。
    void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
    

    1.6 延迟异步派发 dispatch_after

    // 等待一段时间后,异步派发。
    // when:returned by dispatch_time or dispatch_walltime。
    // 内部会调用 Block_copy 、 Block_release。
    void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
         // do something
    });
    

    1.7 dispatch_once

    // 在 app 生命周期中,只执行一次,可用于单例模式。
    // predicate:必须是全局或静态变量,用于判断 block 是否执行了。
    // 多线程环境下,外面的线程会同步等待直到 block 执行完。
    void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
       // do something     
    });
    

    1.8 并发迭代 dispatch_apply

    // 同步并发迭代,可以提高效率。
    // 阻塞当前线程,分成多个部分,在多个线程并发循环,直到迭代完成。
    // Using this function with a concurrent queue can be useful as an efficient parallel for loop.
    // iterations:迭代次数。
    // index:当前迭代的次数,从 0 开始。
    // queue:最好是并发队列。
    void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t index));
    
    // 可能在 10 个线程并发迭代 10 次,总共 100 次。
    NSLog(@"开始 dispatch_apply");
    dispatch_apply(100, global, ^(size_t index) {
        NSLog(@"dispatch_apply %li,  %@", index, [NSThread currentThread]);
    });
    NSLog(@"结束 dispatch_apply");
    

    1.9 暂停 dispatch_suspend

    // 暂停 dispatch queue 或 dispatch source。
    // suspension count +1, > 0 时会暂停,可以多次调用。
    void dispatch_suspend(dispatch_object_t object);
    

    1.10 恢复 dispatch_resume

    // 恢复执行。
    // suspension count -1, = 0 时会恢复,可以多次调用。
    void dispatch_resume(dispatch_object_t object);
    

    2、使用 dispatch group

    在讲解 API 之前,先看下最常见的用法:所有异步操作完成后(比如多个接口拿数据),追加执行一个同步操作。

    - (void)testDispatchGroup {
        // group 会在所有任务完成后释放
        dispatch_group_t group = dispatch_group_create();
        // queue 会在所有 block 完成后释放
        dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
        
        // 任务数是指 dispatch_group_t 内部的一个 count 属性。
        // 任务数 +1
        dispatch_group_async(group, queue, ^{
            [NSThread sleepForTimeInterval:3];
            NSLog(@"group async 完成");
            // block 执行后,任务数 -1
        });
        
        dispatch_group_enter(group); // 任务数 +1
        // 异步下载图片
        [self downloadImageWithComplete:^(UIImage *image) {
            NSLog(@"group downloadImage 完成");
            dispatch_group_leave(group); // 任务数 -1
        }];
        
        // 所有任务完成,即任务数 = 0 时,会提交 block 到 queue。
        dispatch_group_notify(group, queue, ^{
            NSLog(@"group notify");
        });
        
        // 控制台输出
    //    2019-06-13 21:06:58.911399+0800  group downloadImage 完成
    //    2019-06-13 21:06:59.90973+0800   group async 完成
    //    2019-06-13 21:06:59.910374+0800  group notify
    }
    
    - (void)downloadImageWithComplete:(void(^)(UIImage *image))block {
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        dispatch_async(queue, ^{
            [NSThread sleepForTimeInterval:2];
            block(nil);
        });
    }
    
    

    2.1 dispatch_group_t

    // 管理一组 block,常用于所有 block 执行后,追加执行同步操作。
    // 本质是个信号量。
    // GCD retains the group until all its associated blocks complete execution.
    typedef NSObject<OS_dispatch_group> *dispatch_group_t;
    

    2.2 创建组 dispatch_group_create

    // 创建一个 group,内部有个 count 属性表示任务数。
    dispatch_group_t dispatch_group_create(void);
    

    The dispatch group maintains a count of its outstanding associated tasks, incrementing the count when a new task is associated and decrementing it when a task completes. Functions such as dispatch_group_notifyand dispatch_group_wait use that count to allow your application to determine when all tasks associated with the group have completed.

    2.3 dispatch_group_async

    // 异步添加一个 block 到 group。
    // group 的任务数 count +1,执行完 count -1。
    // group 和 queue 会在所有任务完后释放。
    void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
    

    2.4 dispatch_group_enter

    // group 的任务数 count +1。
    // 一个 block 可以同时 enter 多个 group。
    void dispatch_group_enter(dispatch_group_t group);
    

    2.5 dispatch_group_leave

    // group 的任务数 count -1。
    // 调用次数要和 dispatch_group_enter 配对。
    void dispatch_group_leave(dispatch_group_t group);
    

    2.6 dispatch_group_notify

    // group 所有的任务完成后,提交 block 到 queue。
    // 如果 group 没有任务,会立即提交。
    void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
    

    2.7 dispatch_group_wait

    // 阻塞当前线程,直到所有任务完成或超时。
    // timeout:通过 dispatch_time 创建,可以是 DISPATCH_TIME_NOW 或 DISPATCH_TIME_FOREVER。
    // 返回值:任务完成、没有超时,返回 0。超时失败返回非零。
    long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
    

    3、使用 dispatch_barrier

    3.1 dispatch_barrier_async

    // block 执行后,在它之后提交的 block 才能执行。
    // 注意,必须是自己创建的并行队列。
    // 如果是串行队列或系统的并发队列,会按 dispatch_async 处理。
    void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
    
    - (void)testDispatchGroupBarrier {
        // 注意,这里不能用全局队列和串行队列。后一个参数传 NULL 会是串行队列。
        dispatch_queue_t queue = dispatch_queue_create("com.my.queue", DISPATCH_QUEUE_CONCURRENT);
    
        dispatch_async(queue, ^{
            NSLog(@"dispatch_async A");
        });
        
        dispatch_async(queue, ^{
            NSLog(@"dispatch_async B");
        });
        
        dispatch_barrier_async(queue, ^{
            [NSThread sleepForTimeInterval:2];
            NSLog(@"dispatch_barrier_async ");
        });
    
        dispatch_async(queue, ^{
            NSLog(@"dispatch_async C");
        });
        
    //    控制台输出
    //    2019-06-14 10:57:42.283973  dispatch_async A
    //    2019-06-14 10:57:42.284953  dispatch_async B
    //    2019-06-14 10:57:44.297776  dispatch_barrier_async
    //    2019-06-14 10:57:44.313885  dispatch_async C
    
    }
    

    block 提交之后,要等在它之前提交的 block 执行完,它才会执行,然后在它之后提交的 block 才能执行。
    Calls to this function always return immediately after the block has been submitted and never wait for the block to be invoked. When the barrier block reaches the front of a private concurrent queue, it is not executed immediately. Instead, the queue waits until its currently executing blocks finish executing. At that point, the barrier block executes by itself. Any blocks submitted after the barrier block are not executed until the barrier block completes.

    注意
    The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_async function.

    3.2 dispatch_barrier_sync

    // 会阻塞当前线程,直到 block 执行完。block 执行后,在它之后提交的 block 才能执行。
    // 注意,必须是自己创建的并行队列。
    // 如果是串行队列或系统的全局队列,会按 dispatch_sync 处理。
    void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
    

    4、信号量

    信号量可以控制最大并发数,如果最大并发数是 1,就是一种线程锁。也可以把异步操作变成同步操作,具体看下面的例子。
    使用步骤是,首先创建信号量,信号量的值等于资源数。进入临界区前,调用 dispatch_semaphore_wait,如果传入的信号量的值等于 0,线程就会等待。出了临界区就调用 dispatch_semaphore_signal,其他正在等待的线程就可以进入临界区了。

    4.1 dispatch_semaphore_t

    typedef NSObject<OS_dispatch_semaphore> *dispatch_semaphore_t;
    

    4.2 创建 dispatch_semaphore_create

    // value:不要 < 0。两个线程的话传 0 可以控制竞争(这里文档可能有误,我觉得应该传 1)。
    // 多线程可以传 > 0,value 值 = 资源数。
    dispatch_semaphore_t dispatch_semaphore_create(long value);
    

    4.3 dispatch_semaphore_wait

    // 信号量先 -1,如果值 < 0,就阻塞当前线程。
    // 超时返回 非0,成功返回 0。
    // timeout:等待时长,可以是 DISPATCH_TIME_FOREVER (无限等待)。
    // timeout:dispatch_time(DISPATCH_TIME_NOW, 秒 * NSEC_PER_SEC)
    // 注意,之后一定要调用 dispatch_semaphore_signal,即使没有超时。
    long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
    
    
    // 猜猜 value 为 0 和 1 时,分别输出什么?
    - (void)testDispatchSemaphore {
        long value = 0;
    //    long value = 1;
    
        dispatch_semaphore_t sema = dispatch_semaphore_create(value);
        
        NSLog(@"A");
        
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        NSLog(@"B");
    
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        NSLog(@"C");
        
    //    如果 value 初始值是 1,输出 A、B
    //    如果 value 初始值是 0,输出 A
    //    说明信号量是先 -1,如果 < 0 就阻塞当前线程
        
    }
    

    Decrement the counting semaphore. If the resulting value is less than zero, this function waits for a signal to occur before returning.

    4.4 dispatch_semaphore_signal

    // 信号量 +1。如果之前信号量 < 0,则唤醒正在等待的线程。
    // 唤醒线程则返回 非 0,否则返回 0。
    // returns non-zero if a thread is woken. Otherwise, zero is
     * returned
    long dispatch_semaphore_signal(dispatch_semaphore_t dsema);
    

    4.5 信号量的一个 demo

    这是 AFNetworking 的代码。getTasksWithCompletionHandler 是一个异步函数,这里信号量的作用是把异步操作变成同步操作,先阻塞当前线程,等待异步操作完成后,再恢复当前线程往下走。

    - (NSArray *)tasksForKeyPath:(NSString *)keyPath {
        __block NSArray *tasks = nil;
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
        [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
            if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
                tasks = dataTasks;
            } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
                tasks = uploadTasks;
            } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
                tasks = downloadTasks;
            } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
                tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
            }
    
            dispatch_semaphore_signal(semaphore);
        }];
    
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
        return tasks;
    }
    
    

    5、dispatch_time

    5.1 dispatch_time_t

    1、
    // 时间定义,用于 dispatch_after 等。
    // 常用的:DISPATCH_TIME_NOW、DISPATCH_TIME_FOREVER
    typedef uint64_t dispatch_time_t;
    
    2、
    // 两个时间常量的定义
    
    // Indicates a time that occurs immediately.
    #define DISPATCH_TIME_NOW (0ull)
    
    // Indicates a time that means infinity.
    #define DISPATCH_TIME_FOREVER (~0ull)
    
    3、
    // 几个用于计算的时间单位
    
    // 毫秒 millisecond,微秒 microsecond,纳秒 nanosecond
    // 秒 SEC,毫秒 MSEC,微秒 USEC,纳秒 NSEC
    // 1秒=1000毫秒=1000*1000微秒=1000*1000*1000毫微秒(纳秒)
    
    // 1 秒的纳秒数,常用
    #define NSEC_PER_SEC 1000000000ull
    
    // 1 毫秒的纳秒数
    #define NSEC_PER_MSEC 1000000ull
    
    // 1 秒的微秒数
    #define USEC_PER_SEC 1000000ull
    
    // 1 微秒的纳秒数
    #define NSEC_PER_USEC 1000ull
    
    
    // 比如以纳秒为单位的3秒可以表示为:
    3 * NSEC_PER_SEC
    
    

    5.2 dispatch_time

    // 创建或修改时间。
    // when:新值的基础值,比如 DISPATCH_TIME_NOW。
    // delta:基础值的增量,nanosecond,毫微秒(纳秒)
    // 注意,1秒=1000毫秒=1000*1000微秒=1000*1000*1000毫微秒(纳秒)
    dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);
    
    
    // 比如 3秒 后执行
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
    dispatch_after(time, dispatch_get_main_queue(), ^{
         // do something
    });
    
    

    5.3 dispatch_walltime

    // 创建一个绝对时间(absolute time)。
    // when:新值的基础值,传 NULL 相当于 DISPATCH_WALLTIME_NOW。
    // delta:基础值的增量,毫微秒(纳秒)。
    dispatch_time_t dispatch_walltime(const struct timespec *when, int64_t delta);
    

    dispatch_walltime 和 dispatch_time 的区别是,前者创建的是绝对时间,后者是相对时间。比如用于 dispatch_after,如果设备休眠了,相对时间会暂停计时,唤醒后继续计时。而绝对时间在休眠时,也会继续计时,因为真实世界的时间在流逝。


    6、使用 dispatch_block_t

    6.1 dispatch_block_t

    // 定义
    typedef void (^dispatch_block_t)(void);
    

    The declaration of a block allocates storage on the stack. Therefore, this example demonstrates an invalid construct:

    // 错误的代码示范
    
    dispatch_block_t block;
     
    if (x) {
        block = ^{printf("true\n"); };
    } else {
        block = ^{printf("false\n"); };
    }
    block();  // unsafe!!
    

    What is happening behind the scenes:

    if (x) {
        struct Block __tmp_1 = ...;  // setup details
        block = &__tmp_1;
    }  else {
        struct Block __tmp_2 = ...; // setup details
        block = &__tmp_2;
    }
    

    As the example demonstrates, the address of a stack variable is escaping the scope in which it is allocated.

    6.2 相关函数

    dispatch_block_create 创建 block 对象,
    dispatch_block_perform 执行 block,
    dispatch_block_notify 可以监听 block 完成并追加执行代码,dispatch_block_cancel 可以取消还没开始执行的 block。
    要用到这些功能,不如直接使用 NSOperation。感兴趣的去官网看一下,这里就不讲解了。



    错误之处,欢迎指正。

    相关文章

      网友评论

          本文标题:GCD

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