多线程总结之GCD

作者: 西南柯北 | 来源:发表于2017-05-31 17:22 被阅读69次

    多线程有几个重要的概念,任务、队列、线程。
    任务:是指执行什么样的操作,在GCD中就是block。
    队列:用来存放任务,分为串行队列和并行队列。放在串行队列中的任务要等到正在执行的任务执行完后才会被执行;放在并行队列中的任务不用等到正在执行的任务执行完就可以被执行。
    线程:执行任务需要线程,线程从队列中以先进先出(FIFO)的方式取出任务执行,线程一次只能执行一个任务。
    这三者的关系就是:线程从队列中取任务执行。

    GCD中的队列:
    dispatch_main_queue 主队列,程序启动时与主线程一起由系统自动创建,UI操作都放在主队列中
    dispatch_get_global_queu 全局并发队列,由系统定义,调用函数dispatch_get_global_queue(identifier: Int, flags: Uint)获取。identifier以前叫做优先级,现在称为服务质量,现在传入优先级的宏定义也可以,目测优先级的界限并不明显,所以平时直接传入0,见下面对应关系。flags是系统保留的一个参数,传入0即可。

    DISPATCH_QUEUE_PRIORITY_HIGH        QOS_CLASS_USER_INITIATED       2
    DISPATCH_QUEUE_PRIORITY_DEFAULT     QOS_CLASS_DEFAULT              0
    DISPATCH_QUEUE_PRIORITY_LOW         QOS_CLASS_UTILITY             -2
    DISPATCH_QUEUE_PRIORITY_BACKGROUND  QOS_CLASS_BACKGROUND           INT16_MIN
    

    对于同一优先级,获取到的全局并发队列是同一个; 不同优先级,获取到的全局并发队列也不同。看下面获取全局并发队列

    NSLog(@"%@",dispatch_get_global_queue(0, 0));
    NSLog(@"%@",dispatch_get_global_queue(0, 0));
    NSLog(@"%@",dispatch_get_global_queue(2, 0));
    NSLog(@"%@",dispatch_get_global_queue(2, 0));
    
    运行结果
    dispatch_queue_create(<#const char * _Nullable label#>, <#dispatch_queue_attr_t  _Nullable attr#>)
    

    开发者自己创建的队列,参数label是该队列的名字,可以通过

    dispatch_queue_get_label(<#dispatch_queue_t  _Nullable queue#>)
    

    获取。参数attr传入DISPATCH_QUEUE_SERIAL或nil返回串行队列,传入DISPATCH_QUEUE_CONCURRENT返回并行队列。
    dispatch_queue_create 创建的优先级与全局并发队列的默认优先级是同一级别,也就是说创建的队列的优先级为默认优先级。至于怎么改变它的优先级需要调用dispatch_set_target_queue,这个函数后面描述。

    调度函数

    dispatch_sync 将任务提交到相应队列,同步执行,不开新线程
    dispatch_async 将任务提交到相应队列,异步执行,如果从主队列取任务,不开新线程,在主线程中执行;其他队列,开新线程执行
    现在来看看调度函数与各种队列的组合后的情况:
    1、dispatch_sync 与 串行队列:
    (1)在串行队列queue中调用该函数、且该函数是从串行队列queue中去任务执行,就会出现线程死锁。 例如下面两种情况:

    在主线程中调用

    dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"在主线程中调用dispatch_sync,并且该函数第一个参数传入的是主队列,则会出现线程死锁");
        });
    

    在同步执行任务的子线程中调用

    dispatch_queue_t queue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
        dispatch_async(queue, ^{
            /*
             其他代码
             */
            dispatch_sync(queue, ^{
                NSLog(@"queue是串行队列就会出现线程死锁");
            });
        });
    

    (2)该串行队列与当前队列不同,不会出现线程死锁。在当前线程中同步执行。dispatch_sync执行完后它后面的代码才会执行。

    2、dispatch_sync 与 并行队列:
    因为dispatch_sync不开线程,并且线程一次只能执行一个任务,所以依然是,dispatch_sync执行完后它后面的代码才会执行。
    3、dispatch_async 与 串行队列:
    (1)主队列,不开新线程,任务将在主线程中执行。但不会马上执行,而是将任务从栈copy到堆,等待主队列中栈区的任务执行完,再执行堆区的任务。因此

    dispatch_async(dispatch_get_main_queue(), ^{
            /*
             在主线程执行任务
             */
        });
    

    有两个作用:1)子线程处理耗时操作后回到主线程处理UI; 2)延时。
    (2)开发者创建的串行队列,开新线程同步执行。
    4、dispatch_async 与 并发队列:
    开新线程并发执行任务,至于开多少条线程由系统决定。看下面的代码以及运行结果:

    for (int i = 0; i < 20; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"线程%@执行第%d个任务",[NSThread currentThread],i);
        });
    }
    
    运行结果 number从3到14,新开线程个数<20

    dispatch_after 延时操作,调用函数

    dispatch_after(<#dispatch_time_t when#>, <#dispatch_queue_t  _Nonnull queue#>, <#^(void)block#>)
    

    参数when 指定时间,传入DISPATCH_TIME_NOW,效果等同于dispatch_async;传入DISPATCH_TIME_FOREVER,任务将永不执行;所以一般传入dispatch_time(DISPATCH_TIME_NOW, (int64_t)(t * NSEC_PER_SEC))
    t*NSEC_PER_SEC代表t秒。
    参数queue可以是任意队列,一般传入主队列。

    dispatch_group 调度组
    几个任务并发执行,并且等到都执行完后再做其他操作,就可调使用该函数。例如异步下载小说A、B、C,下载完后提示用户,代码如下:

        dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        dispatch_group_async(group, queue, ^{
            NSLog(@"下载小说A");
        });
        dispatch_group_async(group, queue, ^{
            NSLog(@"下载小说B");
        });
        dispatch_group_async(group, queue, ^{
            NSLog(@"下载小说C");
        });
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"所有小说下载完成");
        });
    
    运行结果

    也可将

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"所有小说下载完成");
    });
    

    替换成

    BOOL flag = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    if (flag) {
        NSLog(@"所有小说下载完成");
    } else {
        NSLog(@"小说下载超时");
    }
    

    dispatch_group_wait的第二个参数可传入指定时间。

    dispatch_once 一次性操作
    调用函数

    dispatch_once(<#dispatch_once_t * _Nonnull predicate#>, <#^(void)block#>)
    

    参数predicate 是用来标识block是否执行的指针,必须是全局或静态变量,并且该指针所指向的区域的初始值为0,当任务执行完毕,系统会将其值置为-1。
    这个函数用于初始化全局数据,并且保证线程安全,常用于OC中的单例模式。
    dispatch_apply dispatch_sync 与 dispatch_group 的结合

    调用函数

    dispatch_apply(<#size_t iterations#>, <#dispatch_queue_t  _Nonnull queue#>, <#^(size_t)block#>)
    

    参数 iterations 将任务循环添加到指定队列的次数; block的参数是循环下标(从0开始)
    调用该函数会将queue中的任务执行完才执行它后面的代码。queue如果是串行队列,queue中的任务会在当前线程中同步执行;queue如果是并行队列,当前线程会取出queue中的第一个任务执行,然后开新线程执行后面的任务,所开线程个数由系统决定。因为dispatch_apply具有dispatch_sync的特征,所以如果queue是串行队列并且与当前的队列是同一个对象时,就会出现线程死锁。
    dispatch_apply主要用于异步并发处理数据,并且处理完后统一操作。所以dispatch_apply一般在子线程中调用。当然,像这样的操作用dispatch_group也可以实现。但是,思考这种情况,如果对象数组,里面所有元素需要并发执行某种操作,并且都执行完之后要统一做处理,这时如果用dispatch_group,则需要for in 遍历数组,用dispatch_apply则可以省去这种遍历。代码如下:

    dispatch_async(queue, ^{
        dispatch_apply(arr.count, dispatch_get_global_queue(0, 0), ^(size_t i) {
            NSLog(@"%@",arr[i]);
        });
    });
    

    dispatch_barrier_async 在处理数据读取时,为了避免数据竞争的问题,写入操作与写入操作或读取操作不能并发执行。针对这个例子,可以使用dispatch_barrier_async将写入操作提交到并发队列中,此时被提交的任务暂不执行,当他前面的任务执行完毕时在执行该任务,等到该任务执行完毕,后面提交的才执行。需要注意的是指定的queue应该是通过dispatch_queue_create创建的,系统定义的全局并发队列无效。示例代码如下:

    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"读取操作1");
    });
    dispatch_async(queue, ^{
        NSLog(@"读取操作2");
    });
    dispatch_async(queue, ^{
        NSLog(@"读取操作3");
    });
    dispatch_barrier_async(queue, ^{
        NSLog(@"写入操作1");
    });
    dispatch_async(queue, ^{
        NSLog(@"读取操作4");
    });
    dispatch_async(queue, ^{
        NSLog(@"读取操作5");
    });
    dispatch_async(queue, ^{
        NSLog(@"读取操作6");
    });
    dispatch_barrier_async(queue, ^{
        NSLog(@"写入操作2");
    });
    dispatch_async(queue, ^{
        NSLog(@"读取操作7");
    });
    dispatch_async(queue, ^{
        NSLog(@"读取操作8");
    });
    
    运行结果
    与之对应的是dispatch_barrier_sync,该函数会阻塞当前线程。它也会等待前面提交的任务执行完毕在执行该函数提交的任务,等它执行完毕后面的任务才提交到queue中执行

    dispatch_semaphore 信号量。 简单来说就是控制访问资源的数量,比如系统有两个资源可以被利用,同时有三个线程要访问,只能允许两个线程访问,第三个应当等待资源被释放后再访问。
    下面逐一介绍与之相关的三个函数:
    (1)dispatch_semaphore_create(<#long value#>) 创建信号量,返回值类型dispatch_semaphore_t
    参数value 为允许访问资源的线程数,该值必须 >= 0,否则会返回nil。当value为0时对访问资源的线程没有限制。
    (2)dispatch_semaphore_signal(<#dispatch_semaphore_t _Nonnull dsema#>) 信号量+1
    (3)dispatch_semaphore_wait(<#dispatch_semaphore_t _Nonnull dsema#>, <#dispatch_time_t timeout#> 该函数会让信号量-1

    这个函数的具体作用是这样的,如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1,返回值为0;如果desema的值为0,那么这个函数就阻塞当前线程等待timeout,如果等待的期间desema的值被dispatch_semaphore_signal函数加1了,且该函数所处线程获得了信号量,那么就继续向下执行并将信号量减1, 返回值为0。如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句,返回值大于0。通过返回值是否为0,可以判断等待是否超时。

    第二个函数和第一个函数是配套使用的,使用时先调用函数(3)将信号量-1,后调用函数(2)将信号量+1

    为了加深理解,举一个在餐厅排队吃放的例子。假设某餐厅有50个座位,这相当于调用dispatch_semaphore_wait(value: Int)参数传入50,有顾客来吃饭相当于函数dispatch_semaphore_wait(dsema: dispatch_semaphore_t, timeout: dispatch_time_t),前50个顾客都有座位直接就餐。第51个顾客来到时就需要等待前面的顾客吃完饭离开,他才能就坐用餐。此时相当于dispatch_semaphore_wait函数阻塞当前线程。当有顾客用餐完毕离开时,此时就有了一个空位。相当于dispatch_semaphore_signal将型号来加1,第51个顾客等到座位,相当于dispatch_semaphore_wait返回值为0。

    信号量是GCD同步的一种方式。前面介绍过dispatch_barrier_async是对queue中的任务进行批量同步处理,dispatch_sync是对queue中的任务单个同步处理,而dispatch_semaphore是对queue中的某个任务中的某部分(某段代码)同步处理。此时将dispatch_semaphore_wait中的参数传入1。
    dispatch_semaphore 的使用如下:

    for (int i = 0; i < 100; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            /*
             其他并发操作
            */
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            /*
             同步操作,例如 [arr addObject:@(i)];
             */
            [arr addObject:@(i)];
            dispatch_semaphore_signal(semaphore);
            /*
             其他并发操作
             */
        });
    }
    

    dispatch_suspend / dispatch_resume 暂停指定的队列 / 恢复指定的队列
    有时候获取希望提交到queue中的任务暂不执行,等待某一时刻执行,这时候就可使用dispatch_suspend 和 dispatch_resume, 使用dispatch_suspend时对正在执行的任务不会起作用。

    dispatch_suspend使指定队列的暂停计数+1,dispatch_resume使指定队列的暂停计数-1。
    使用它们时要注意几点:
    (1)dispatch_suspend 与 dispatch_resume 成对出现 ,否则程序在运行时会crash
    (2)dispatch_suspend 与 dispatch_resume 中指定的队列是通过 dispatch_queue_create 创建的,其他队列无效

    dispatch_set_target_queue 前面提到过,我们自己创建的队列的优先级是默认优先级,需要更改优先级需要调用dispatch_set_target_queue(系统的队列优先级不能修改)。实际上,dispatch_set_target_queue 主要有两个作用:
    (1)更改 dispatch_queue_create 函数创建的队列的优先级。代码如下:

    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_set_target_queue(queue, dispatch_get_global_queue(0, 0));
    

    函数中第一个是需要变更优先级的队列,第二个参数是需要传入全局并发队列,并且优先级是第一个参数想要的优先级。
    (2)修改用户队列的目标队列,使多个serial queue在目标queue上一次只有一个执行。第二个参数是目标队列,第一个参数是任务将放在目标队列中的另一串行队列。
    例如,用户要依次下载小说A、B、C,但下载任务放在不同的串行队列中,这时就可以依次调用dispatch_set_target_queue,将放有下载任务的队列作为第一个参数传入,让任务将目标队列中同步执行。 示例代码如下:

    dispatch_queue_t targetQueue = dispatch_queue_create("targetQueue", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_SERIAL);
        dispatch_queue_t queue3 = dispatch_queue_create("queue3", DISPATCH_QUEUE_SERIAL);
        dispatch_set_target_queue(queue1, targetQueue);
        dispatch_set_target_queue(queue2, targetQueue);
        dispatch_set_target_queue(queue3, targetQueue);
        dispatch_async(queue1, ^{
            NSLog(@"开始下载小说A");
            /*
             下载中
             */
            NSLog(@"小说A下载完成");
        });
        dispatch_async(queue2, ^{
            NSLog(@"开始下载小说B");
            /*
             下载中
             */
            NSLog(@"小说B下载完成");
        });
        dispatch_async(queue3, ^{
            NSLog(@"开始下载小说C");
            /*
             下载中
             */
            NSLog(@"小说C下载完成");
        });
    
    运行结果

    相关文章

      网友评论

        本文标题:多线程总结之GCD

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