请稍侯

深入理解 iOS 中 GCD 原理

08 December 2020

深入理解 iOS 中 GCD 原理

首先了解一下,计算机程序是怎样运行的,然后再具体讲 GCD 相关内容。

  1. 当开始运行程序时,首先应把第一条指令所在存储单元的地址赋予程序计数器PC,然后机器就进入了取指阶段
  2. 在取指阶段,CPU从内存中读取的内容必为指令。然后送往数据缓冲寄存器DR,经DR送往指令寄存器IR;
  3. 之后由指令译码器ID对IR中的指令的操作码字段进行译码,然后又PLA发出执行该指令所需要的各种微操作控制信号,取指阶段到此结束。
  4. 之后,机器就进入执行指令阶段,这时CPU便执行指令所规定的具体操作。
  5. 当一条指令执行完毕后,即转入下一条指令的取指阶段,如此循环往复,知道遇到暂停或程序结束为止。

下面看一下GCD主要的API

串行队列与并行队列

在说GCD之前,我们先彻底弄明白串行队列与并行队列的意义和作用。

  • CPU执行指令是一条条执行的,指令的执行序列是一条按执行时间先后顺序排列的无分叉路径,我们这里管这个叫指令执行流,这个执行流可以是进程或线程,或是某些语言里所说的worker也好;
  • CPU按时间片执行指令,以及多核CPU的出现,可以让1个CPU执行多条不同路径的指令执行流;
  • 我们的程序通过编译链接后,最终得到是一个二进制形式的机器码指令的集合;
  • 如果CPU的每个时钟脉冲会执行1条指令,那一个3G Hz的CPU在1秒里就可以执行30亿条指令,当然实际上1条计算机指令可能由多个CPU微指令的组合操作构成;
  • 队列一般会添加到一个执行流上的,在一个时间段里一个执行流上只能有一个队列在执行;
  • 我们Block代码最终是以结构体对象的形式添加到队列上的;一个队列上可以添加多个Block代码执行对象;添加在GCD队列上的对象也称之为任务对象;
  • 一个串行队列里的所有对象只会分配到同一个执行流上执行,一个并行队列里的对象会分配到多个不同执行流上执行,所以一个串行队列只对应一个执行流,一个并行队列就可能对应多个执行流;
  • 所以主队列dispatch_get_main_queue一定肯定是串行队列,因为它只在主线程运行,而全局队列dispatch_get_global_queue则是并行队列,这个全局并行队列上的任务具体被分配在哪个执行流上执行,是由系统调试决定,若主线程不忙一般还是分配在主线程上执行;
  • dispatch_async会对作为参数传入的队列里追加一个Block任务对象,不会阻塞当前队列的执行流,异步执行作为参数传入的队列里的任务;
  • dispatch_sync会对作为参数传入的队列里追加一个Block任务对象,只会阻塞主队列的执行流,并且会同步执行作为参数传入的队列里的任务;
  • dispatch_barrier_async栅栏函数会等待队列上已有的任务全部完成后,再追加栅栏函数传入的任务到队列,并异步执行此任务;而dispatch_barrier_sync的差别是同步执行此任务;
  • dispatch_apply在传入的队列里迭代block任务指定次数;
  • dispatch_suspend用于挂起队列或资源;dispatch_resume用于恢复队列或资源;
  • dispatch_once在程序的生命周期范围内只执行一次;但是并不是简单的只执行一次,dispatch_once本质上可以接受多次请求,会对此维护一个请求链表,用来保存程序中所有的单例dispatch_once的静态标记。

队列与dispatch_async的本质

dispatch_asyncdispatch_sync 与队列的使用,下面用一段代码来彻底弄明白。

int main(int argc, const char *argv[]) {
    //代码添加到autoreleasepool中,使用dispatch_get_global_queue系统会优化线程的创建与分配
    @autoreleasepool {
        
        //验证:dispatch_sync不会阻塞全局队列的执行流
        dispatch_queue_t glb_q = dispatch_get_global_queue(0, 0);
        dispatch_sync(glb_q, ^{ //a添加到全局队列异步执行,
            NSLog(@"a dispatch_sync 调用内部,线程:%@", NSThread.currentThread);
            dispatch_sync(glb_q, ^{ //b添加到全局队列异步执行,
                NSLog(@"b dispatch_sync 调用内部,线程:%@", NSThread.currentThread);
            });
        });
        
        /* 下面第1个数字表示执行流顺序;m表示主队列,g表示全局队列,后面的数字表示任务在队列中的位置,
         如:m2表示主队列中的第2个任务;t表示线程,t1表示主线程,t2表示其他线程。
         */
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"2_m2_t1. dispatch_async 调用内部,线程:%@", NSThread.currentThread);
        });
        NSLog(@"1_m1_t1. dispatch_async 调用后,线程:%@", NSThread.currentThread);
        [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];

        dispatch_queue_t global_q = dispatch_get_global_queue(0, 0);
        dispatch_sync(global_q, ^{
            NSLog(@"3_g1_t1. dispatch_sync 调用内部,线程:%@", NSThread.currentThread);
            //因当前主线程已经加了同步锁,所以这里添加任务到主队列只能才采用异步方式添加
            dispatch_async(dispatch_get_main_queue(), ^{ 
                NSLog(@"6_m4_t1. dispatch_async 调用内部,线程:%@", NSThread.currentThread);
            });

            dispatch_async(global_q, ^{ //t7添加到全局队列异步执行
                NSLog(@"7_g3_t2. dispatch_sync 调用内部,线程:%@", NSThread.currentThread);
            });
            dispatch_sync(global_q, ^{
                NSLog(@"4_g2_t1. dispatch_sync 调用内部,线程:%@", NSThread.currentThread);
            });
        });

        dispatch_async(global_q, ^{
            //因为t7与t8都在同一个队列上,并且t7早于t8加入队列,所以如果t7与t8分本在同一个线程上的话,t7会早于t8执行;
            NSLog(@"8_g4_t2. dispatch_sync 调用内部,线程:%@", NSThread.currentThread);
        });
        NSLog(@"5_m3_t1. dispatch_sync 调用后,线程:%@", NSThread.currentThread);

        [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
        
    }
    return 0;
}

这个代码会输出:

a dispatch_sync 调用内部,线程:<NSThread: 0x100505840>{number = 1, name = main}
b dispatch_sync 调用内部,线程:<NSThread: 0x100505840>{number = 1, name = main}
1_m1_t1. dispatch_async 调用后,线程:<NSThread: 0x100505840>{number = 1, name = main}
2_m2_t1. dispatch_async 调用内部,线程:<NSThread: 0x100505840>{number = 1, name = main}
3_g1_t1. dispatch_sync 调用内部,线程:<NSThread: 0x100505840>{number = 1, name = main}
4_g2_t1. dispatch_sync 调用内部,线程:<NSThread: 0x100505840>{number = 1, name = main}
7_g3_t2. dispatch_sync 调用内部,线程:<NSThread: 0x100520de0>{number = 2, name = (null)}
8_g4_t2. dispatch_sync 调用内部,线程:<NSThread: 0x100520de0>{number = 2, name = (null)}
5_m3_t1. dispatch_sync 调用后,线程:<NSThread: 0x100505840>{number = 1, name = main}
6_m4_t1. dispatch_async 调用内部,线程:<NSThread: 0x100505840>{number = 1, name = main}

GCD 中主要的API

队列

dispatch_get_main_queue() //取主队列
dispatch_get_global_queue() //取全局队列,第1个参数是线程执行的优先级,第2个参数是预留参数0
dispatch_queue_create() //创建队列,第1个参数标签名,第2个参数NULL表示创建串行队列,DISPATCH_QUEUE_CONCURRENT表示创建并行队列

执行

dispatch_async() //异步执行,即在队列里追加一个block对象的任务,不阻塞当前队列的执行流;
dispatch_sync() //同步执行,即在当主队列追加一个block对象的任务,会阻塞主队列的执行流;
dispatch_after() //延后执行
dispatch_once()  //只执行一次
dispatch_apply() //迭代执行指定次数
dispatch_barrier_async() //插入栅栏后异步执行任务
dispatch_barrier_sync()  //插入栅栏后同步执行任务

调度组

dispatch_group_create()  //创建调度组
dispatch_group_async()   //向调度组队列里添加任务
dispatch_group_enter()   //每次调用后任务向调度组列队里添加1次,并且计数+1;
dispatch_group_leave()   //每次调用后把在调度组列队里的任务计数计数-1,减到0时从队列里移出任务;
dispatch_group_notify()  //调度组列队中的任务全部执行完后通知block执行
dispatch_group_wait()    //等待整个调度组所有任务执行完

dispatch_group_enter 必须与 dispatch_group_leave 成对出现;

信号量

//dispatch_semaphore_t对象,参数value用于初始化 semaphore.count;
//semaphore.count小于等于0会阻塞当前线程;
dispatch_semaphore_create() 
dispatch_semaphore_wait()  //p操作,对dsema进行原子性的-1,阻塞
dispatch_semaphore_signal() //V操作,对dsema进行原子性+1,唤醒

调度资源

dispatch_source_create()
dispatch_source_set_timer()
dispatch_source_set_event_handler()
dispatch_resume()
dispatch_suspend()
dispatch_source_cancel()
dispatch_source_testcancel()
dispatch_source_set_cancel_handler()

使用调度组dispatch_group_t

dispatch_group_t可以用来将GCD的任务合并到一个组里来管理,也可以同时监听组里所有任务的执行情况; 在group上任务完成前,dispatch_group_wait会阻塞当前线程;dispatch_after延时执行任务;dispatch_block_wait会阻塞当前线程等待;

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        dispatch_queue_t main_q = dispatch_get_main_queue();
        dispatch_queue_t global_q = dispatch_get_global_queue(0, 0);
        dispatch_group_t group = dispatch_group_create();

        dispatch_group_async(group, global_q, ^{NSLog(@"dispatch_async block 1"); });
        dispatch_group_async(group, global_q, ^{NSLog(@"dispatch_async block 2"); });
        dispatch_group_async(group, global_q, ^{NSLog(@"dispatch_async block 3"); });

        //只有当调用组group中的队列global_q的任务都处理完了,才会通知执行以下的这个block
        dispatch_group_notify(group, global_q, ^{NSLog(@"Done!"); });

        dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 800ull * NSEC_PER_MSEC));
        NSLog(@"dispatch_group_wait 后");


        //使用 dispatch_after 让 block1 在1秒后执行
        dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
        dispatch_block_t block1 = ^{NSLog(@"dispatch_after 1秒后 block1"); };
        dispatch_after(time, main_q, block1 );


        //dispatch_block_wait 用于判断block执行是否超出指定时间
        dispatch_async(dispatch_queue_create("ab.d", DISPATCH_QUEUE_CONCURRENT), ^{
            dispatch_block_t block2 = dispatch_block_create(0,^{
                NSLog(@"block2 开始执行");
                [NSThread sleepForTimeInterval:2];
                NSLog(@"block2 结束执行");
            });
            dispatch_async(global_q,block2);
            dispatch_time_t t2 = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
            //dispatch_block_wait 会阻塞当前线程,并判断block2 在 t2时间内是否已执行完毕;
            //dispatch_block_wait 只能作用在使用dispatch_block_create创建的block上;
            if (dispatch_block_wait(block2, t2) == 0) {
                NSLog(@"block2 执行成功");
            } else {
                NSLog(@"block2 执行超时");
            }
        });

        [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
    }
}

输出:

dispatch_async block 2
dispatch_async block 1
dispatch_async block 3
Done!
dispatch_group_wait 
block2 开始执行
block2 执行超时
dispatch_after 1秒后 block1
block2 结束执行

栅栏函数与延迟函数

我们知道dispatch_suspenddispatch_resume也可以用来挂起与恢复队列; 下面来看看相关的代码的实现,如下:

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        dispatch_queue_t q = dispatch_queue_create("ab.c", DISPATCH_QUEUE_CONCURRENT);

        dispatch_async(q, ^{NSLog(@"a reading 1");});
        dispatch_async(q, ^{NSLog(@"a reading 2");});
        dispatch_async(q, ^{NSLog(@"a reading 3");});
        dispatch_barrier_async(q, ^{NSLog(@"a writing");});
        dispatch_async(q, ^{NSLog(@"a reading 4");});
        dispatch_async(q, ^{NSLog(@"a reading 5");});
        dispatch_async(q, ^{NSLog(@"a reading 6");});

        dispatch_sync(q, ^{NSLog(@"b reading 1");});
        dispatch_sync(q, ^{NSLog(@"b reading 2");});
        dispatch_sync(q, ^{NSLog(@"b reading 3");});
        dispatch_barrier_sync(q, ^{NSLog(@"dispatch_barrier_sync: b writing");});
        dispatch_sync(q, ^{NSLog(@"b reading 4");});
        dispatch_sync(q, ^{NSLog(@"b reading 5");});
        dispatch_sync(q, ^{NSLog(@"b reading 6");});

        //在q队列里同步迭代block任务5次
        dispatch_apply(5, q, ^(size_t it){NSLog(@"dispatch_apply %zu",it);});
        NSLog(@"Done");

        //下面模拟飞机起飞
        dispatch_queue_t q1 = dispatch_queue_create("ab.d", DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(q1, ^{NSLog(@"准备");});
        dispatch_async(q1, ^{
            [NSThread sleepForTimeInterval:1];
            NSLog(@"收起落架");
            dispatch_resume(q1);
        });
        dispatch_suspend(q1); //挂起q1队列,让q1无法添加任务
        dispatch_async(q1, ^{NSLog(@"起飞");});

        for(int i=0;i<10;i++){
            static dispatch_once_t predicate;
            dispatch_once(&predicate, ^{
                NSLog(@"dispatch_once %i",i);
            });
        }

        [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
    }
    return 0;
}

输出:

a reading 2
a reading 1
a reading 3
a writing
a reading 4
a reading 5
a reading 6
b reading 1
b reading 2
b reading 3
dispatch_barrier_sync: b writing
b reading 4
b reading 5
b reading 6
dispatch_apply 0
dispatch_apply 1
dispatch_apply 2
dispatch_apply 3
dispatch_apply 4
Done
dispatch_once 0
准备
收起落架
起飞

信号量dispatch_semaphore_t

用dispatch_semaphore_wait函数进行等待(阻塞,对信号量进行减1),直到上一个任务执行完毕后且通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),dispatch_semaphore_wait函数收到信号量之后判断信号量的值大于等于1,会再对信号量的值减1。

__block BOOL isSuccess = NO;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
        isSuccess = YES;
        dispatch_semaphore_signal(semaphore);
    }];
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

dispatch source

  • dispatch source作为苹果封装好的一种数据类型,主要方便我们用于处理事件,并且这些处理的事件在操作系统中处理底层,他主要支持以下种类型的事件的处理:
    1. Timer dispatch sources:定时器类型,能够产生周期性的通知事件;
    2. Signal dispatch sources:信号类型,当UNIX信号到底时,能够通知应用程序;
    3. Descriptor sources:文件描述符类型,处理UNIX的文件或socket描述符,如:数据可读,数据可写,文件被删除、修改或移动,文件的元信息被修改;
    4. Process dispatch sources:进程类型,能够通知一些与进程相关的事件类型,如:当进程退出,当进程调用了forkexec,当一个信号传递给了进程;
    5. Mach port dispatch sources:端口匹配类型,能够通知一些端口事件的类型;
    6. Custom dispatch sources:自定义类型,可以自定义一些事件类型。
  • Dispatch sources可以替换一些异步的回调函数,特别是用于处理一些与系统相关的事件。使用dispatch source可以指定希望监控的事件类型,以及dispatch queue和代码来处理事件,代码的形式可以是block对象或函数。当一个被监听的事件类型的事件到达时,用Dispatch sources指定的block或函数将会被调用执行;
  • 与将任务提交到GCD dispatch queue不同,dispatch sources将会持续对所提交的事件进行监控,除非精确取消所感监听的事件;
  • 为了防止事件被积压在dispatch queue中,dispatch sources实现了一种事件合并机制。如果在上一个事件被放进队列和被执行之前,又来了一个新事件,则dispatch source将合并老事件和新事件。合并可能会替换或更新事件的信息,这取决于事件的类型,这种机制与UNIX系统信号的不排队机制是一样的。

创建Dispatch Sources

创建一个dispatch Sources将涉及两方面的创建过程:创建源事件和dispatch Sources对象。

在创建了源事件之后,则可以按如下的步骤创建dispatch Sources对象:

  1. 使用dispatch_source_create函数来创建dispatch Sources对象;
  2. 配置dispatch Sources对象:
    1. dispatch Sources对象指定一个事件处理句柄;
    2. 若是timer sources类型的事件,则可以调用dispatch_source_set_timer函数来设置timer信息。
  3. 配置dispatch source对象的取消句柄(这个是可选操作);
  4. 调用dispatch_resume函数开始进行事件的处理。

在一个dispatch sources对象被使用之前,需要对其进行一个附加的配置操作。

  • 当调用dispatch_source_create函数来创建一个dispatch sources对象后,该对象仍处于suspended(挂起)状态;
  • 处于挂起状态的dispatch sources对象是可以接收事件的,但不能处理这些事件,这种机制主要是为了给用户时间来配置事件的处理句柄和执行一些附件的配置操作。

配置Event Handler

  • 为了处理dispatch sources对象所产生的事件,用户必须定义一个event handler(事件处理句柄)来执行这些事件;
  • 事件处理句柄可以是一个block对象或是一个函数,可以使用dispatch_source_set_event_handlerdispatch_source_set_event_handler_f函数来配置事件处理句柄;
  • 当一个事件到底时,dispatch source对象会将事件处理句柄投放到dispatch queue中进行执行;
  • 事件处理句柄体的内容负责处理任何到底的事件。如果当一个新事件到达时,而前一个事件处理句柄虽被放入队列,但还未被执行,那么dispatch source将合并两个事件;如果当一个或多个事件到达时,前一个事件的处理句柄已经开始执行,则dispatch source将保存这些事件,直到当前的处理句柄执行后,dispatch source再将事件处理句柄投入队列中;
  • dispatch_source_get_handle返回一个dispatch source监控的数据结构,根据不同的dispatch source类型,则返回的不同语义:
    • 若是描述符类型,则返回一个int类型的文件描述符;
    • 若是信号类型,则返回一个int类型的信号数字;
    • 若是进程类型,则返回一个pid_t类型的数据结构;
    • 若是端口类型,则返回一个端口号;
    • 若是其它类型,则返回的值是不确定的;

      配置Cancellation Handler

      使用dispatch_source_set_cancel_handlerdispatch_source_set_cancel_handler_f函数进行配置。

      修改目标queue

      在创建了dispatch source对象是会 指定eventcancellation handlers运行的queue,之后也可以通过dispatch_set_target_queue函数修改运行的queue

用 GCD 的 API 封装一个定时器

这里我们先看代码实现:

//创建dispatch source
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0, 0, dispatch_get_main_queue());
if (timer){
    uint64_t interval = 1ull * NSEC_PER_SEC;
    uint64_t leeway = 1ull * NSEC_PER_SEC;
     dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval,leeway);
     dispatch_source_set_event_handler(timer, ^{
         NSLog(@"===== timer");
     });
     dispatch_resume(timer);
 }

[NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];

使用dispatch source处理读、写、监听文件事件

  • 创建一个DISPATCH_SOURCE_TYPE_READ类型的dispatch source对象,打开一个filesocket,从文件或网络中读取数据;
  • 创建DISPATCH_SOURCE_TYPE_WRITE类似的dispatch source对象,并配置好写文件描述符,一旦创建了dispatch source对象之后,系统将立即调用event handler来写入数据到filesocket。当完成了写数据,则可以调用dispatch_source_cancel函数来取消dispatch source对象;
  • 可以创建DISPATCH_SOURCE_TYPE_VNODE类型的dispatch source对象,从而当一个文件被删除、写入或重命名等操作时,能够得到通知,实现对文件系统中对象的变化的监控;
  • 使用dispatch source处理读写文件事件,不能把文件描述符配置为阻塞类型的操作;
  • 可以使用dispatch source来异步处理信号,创建一个DISPATCH_SOURCE_TYPE_SIGNAL类型的dispatch source对象,当信息到达时可收到通知,但不可以替代sigaction函数来配置信号处理句柄,也不能不能用于查询所有的signal类型,特别是不能监控SIGILLSIGBUSSIGSEGV信号。
  • 创建一个DISPATCH_SOURCE_TYPE_PROC类型的dispatch source对象可以监控子进程的行为,并进行合适的响应

dispatch source 相关应用代码

取消dispatch source

  • Dispatch source对象将一直保持有效状态,除非手动调用dispatch_source_cancel函数来取消它。
  • 取消了dispatch source对象后,将不能再接收到新的事件。一般情况下是取消了dispatch source后,立即释放掉该对象。
  • 取消dispatch source是一个异步操作,即虽然在调用了dispatch_source_cancel函数之后,dispatch source不能再接收到任何事件,但它还可以继续处理在队列中的事件,直到在队列中的最后一个事件被执行完成后,dispatch source才会执行cancellation handler句柄。

暂停与恢复dispatch source

  • 可以通过使用dispatch_suspenddispatch_resume函数来暂停和恢复事件传递给dispatch source对象;
  • 当暂停了一个dispatch source对象之后,所有在这期间传递给dispatch source对象的事件都会被保存,但当有多个同样事件时,在dispatch source对象恢复之后,会将这些事件合并为一个再发送给dispatch source对象,这与UNIX的信号不排队机制是一样的。

dispatch source使用示例

参考

iOS 并行编程:GCD Dispatch Sources
iOS 中多线程NSOperation NSOperationQueue
GCD定时器使用,封装为单例
ios后台运行
多线程 GCD底层原理
深入理解iOS中的线程关系和使用方法