iOS多线程之GCD

主要涉及到的概念

  • 任务:放在block中的代码,执行方式有 同步执行(sync)异步执行(async)
  • 队列: 串行队列(serial)并发队列(concurrent)
    • 主队列:本质是一个串行队列
    • 全局队列:本质是一个并发队列

知识点:

  • 主队列的任务一定在主线程执行,主线程可以执行非主队列的任务
  • 同步执行不开辟子线程,在当前线程执行
  • 异步执行具有开辟子线程的能力,但是不一定会开辟,后面会有例子
  • 同步执行串行队列按照顺序串行执行
  • 同步执行并发队列依然是按照顺序串行执行
  • 异步执行串行队列会开辟一个子线程,按顺序执行队列任务
  • 异步执行并发队列具备开辟多个子线程的能力

任务的执行方式有两种,队列有四种,共有8种组合,分别为:

同步执行+串行队列

    dispatch_queue_t queueSerial = dispatch_queue_create("com.serial", DISPATCH_QUEUE_SERIAL);
    
    void(^task)(void) = ^{
        NSLog(@"task--%@",[NSThread currentThread]);
    };

    void(^task1)(void) = ^{
        NSLog(@"task1--%@",[NSThread currentThread]);
    };

    dispatch_sync(queueSerial, task);
    dispatch_sync(queueSerial, task1);
    NSLog(@"end");
复制代码

当前是在主线程运行,同步执行会阻塞当前线程(主线程),所以end会在执行完tasktask1之后输出,

dispatch_sync(queueSerial, task);
复制代码
  • 同步执行dispatch_sync阻塞当前线程(主线程)
  • 向串行队列queueSerial中插入任务task
  • 在当前线程(主线程)执行task任务
  • task任务执行完成解除对当前线程(主线程)的阻塞
dispatch_sync(queueSerial, task1); //同理
复制代码
  • 同步执行dispatch_sync阻塞当前线程(主线程)
  • 向串行队列queueSerial中插入任务task1
  • 在当前线程(主线程)执行task1任务
  • task1任务执行完成解除对当前线程(主线程)的阻塞

所以执行顺序为task->task1->end

image.png

同步执行+并发队列

dispatch_queue_t queueConcurrent = dispatch_queue_create("com.conc", DISPATCH_QUEUE_CONCURRENT);

    void(^task)(void) = ^{
        NSLog(@"task--%@",[NSThread currentThread]);
    };

    void(^task1)(void) = ^{
        NSLog(@"task1--%@",[NSThread currentThread]);
    };

    dispatch_sync(queueConcurrent, task);
    dispatch_sync(queueConcurrent, task1);
    NSLog(@"end");
复制代码

在当前线程同步执行无论是串行队列还是并发队列任务都是按照顺序执行,其结果和 同步执行+串行队列 一致

同步执行全局队列

    //第一个参数是队列优先级,第二个参数是保留值默认传0
    dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);

    void(^task)(void) = ^{
        NSLog(@"task--%@",[NSThread currentThread]);
    };

    void(^task1)(void) = ^{
        NSLog(@"task1--%@",[NSThread currentThread]);
    };

    dispatch_sync(queueGlobal, task);
    dispatch_sync(queueGlobal, task1);
    NSLog(@"end");
复制代码

全局队列本质是并发队列,其结果和 同步执行+并发队列 一致

同步执行主队列

    dispatch_queue_t queueMain = dispatch_get_main_queue();

    void(^task)(void) = ^{
        NSLog(@"task--%@",[NSThread currentThread]);
    };

    void(^task1)(void) = ^{
        NSLog(@"task1--%@",[NSThread currentThread]);
    };

    dispatch_sync(queueMain, task); //我也是一个任务 记为 sync1
    dispatch_sync(queueMain, task1);
    NSLog(@"end");
复制代码

这种情况比较特殊,会引起死锁,其原因是同步执行语句dispatch_sync(queueMain, task);本身也是一个任务,我们记为 sync1sync1任务是在当前线程(主线程)同步执行的,sync1任务是向主队列中添加一个任务task并在当前线程(主线程)同步执行任务task,也就是说sync1任务执行完成的前提是task任务执行完成。
因为task任务是在sync1任务之后插入主队列的,所以主线程要先执行完sync1任务才会去执行task任务,而sync1任务执行完成的前提是task任务执行完成,所以就陷入了死锁状态,造成崩溃。

image.png

类似的

    dispatch_queue_t queueSerial = dispatch_queue_create("com.serial", DISPATCH_QUEUE_SERIAL);
    void(^task1)(void) = ^{
        NSLog(@"死锁了吗");
    };

    void(^task2)(void) = ^{
        dispatch_sync(queueSerial, task1);
    };

    //开辟子线程,并向串行队列queueSerial中添加任务 task2
    dispatch_async(queueSerial, task2);
复制代码

dispatch_async(queueSerial, task2);本身也是一个任务记为sync1,但不同的是sync1是在当前线程(主线程)执行,sync1任务执行结果是向串行队列queueSerial中添加任务task2并开辟子线程执行task2,任务task2是向串行队列queueSerial中添加任务task1并在当前线程同步执行任务task1,也就是说task2任务执行完成的前提是task1任务执行完成,因为task2任务比task1任务先添加到串行队列queueSerial,所以任务task2执行完成之后才会去执行tasi1

注意:异步执行+串行队列只会开辟一条子线程

image.png

异步执行串行队列

    dispatch_queue_t queueSerial = dispatch_queue_create("com.serial", DISPATCH_QUEUE_SERIAL);

    void(^task)(void) = ^{
        NSLog(@"task--%@",[NSThread currentThread]);
    };

    void(^task1)(void) = ^{
        NSLog(@"task1--%@",[NSThread currentThread]);
    };

    dispatch_async(queueSerial, task); //我也是一个任务 记为 sync1
    dispatch_async(queueSerial, task1);
    NSLog(@"end");
复制代码

tasktask1分别添加到串行队列queueSerial,开辟一条子线程顺序执行队列queueSerial中的任务,因为end是在主线程输出的,所以输出顺序为end->task->task1,因为异步执行+串行队列只开辟一条子线程,所以tasktask1在同一个线程中执行

image.png

异步执行并发队列

这个时候理论上可以开辟多个子线程了

    dispatch_queue_t queueConcurrent = dispatch_queue_create("com.conc", DISPATCH_QUEUE_CONCURRENT);

    void(^task)(void) = ^{
        NSLog(@"task--%@",[NSThread currentThread]);
    };

    void(^task1)(void) = ^{
        NSLog(@"task1--%@",[NSThread currentThread]);
    };

    dispatch_async(queueConcurrent, task);
    dispatch_async(queueConcurrent, task1);
    NSLog(@"end");
复制代码

因为tasktask1有可能不是在同一个字线程执行的,两个任务不一定谁先结束执行,所以不能确定tasktask1谁先输出,因为end是在主线程输出的,所以先输出end

异步执行主队列

    dispatch_queue_t queueMain = dispatch_get_main_queue();

    void(^task)(void) = ^{
        NSLog(@"task--%@",[NSThread currentThread]);
    };

    void(^task1)(void) = ^{
        NSLog(@"task1--%@",[NSThread currentThread]);
    };

    dispatch_async(queueMain, task);
    dispatch_async(queueMain, task1);
复制代码

主队列本质上也是一个串行队列,所以按照顺序执行task->task1,另外祝队列任务只能在主线程执行,所以tasktask1都在主线程执行

异步执行全局队列

    dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);

    void(^task)(void) = ^{
        NSLog(@"task--%@",[NSThread currentThread]);
    };

    void(^task1)(void) = ^{
        NSLog(@"task1--%@",[NSThread currentThread]);
    };

    dispatch_async(queueGlobal, task);
    dispatch_async(queueGlobal, task1);
复制代码

全局队列是一个并发队列,所以有能力开辟多个子线程,当然也可能在一个子线程中去执行

image.png

线程间通讯

我们需要将一些耗时任务放在子线程,执行完毕之后再回到主线程刷新页面

    dispatch_queue_t queueMain = dispatch_get_main_queue();
    dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);
    dispatch_async(queueGlobal, ^{
        sleep(3);//模拟耗时操作
        dispatch_async(queueMain, ^{
            NSLog(@"主线程");
        });

    });
复制代码

GCD 栅栏方法:dispatch_barrier_async

在《程序员的自我修养:链接、装载与库。》一书的过度优化部分有这么一段话

“CPU的乱序执行能力让我们对多线程的安全保障的努力变得异常困难。因此要保证线程安全,阻止CPU换序是必需的。遗憾的是,现在并不存在可移植的阻止换序的方法。通常情况下是调用CPU提供的一条指令,这条指令常常被称为barrier。一条barrier指令会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。换句话说,barrier指令的作用类似于一个拦水坝,阻止换序“穿透”这个大坝。”

摘录来自: 俞甲子 石凡 潘爱民. “程序员的自我修养:链接、装载与库。” Apple Books.

为了保证某些操作的原子性,CUP提供了barrier指令,用来保证在barrier指令之前的指令执行完成之后才会执行barrier之后的指令,dispatch_barrier_async的意思大体也是如此,在异步执行并发队列中保证先执行完dispatch_barrier_async之前的任务,然后再执行dispatch_barrier_async中的任务,其次执行dispatch_barrier_async之后的任务

image.png

    dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);

    dispatch_async(queueGlobal, ^{
        NSLog(@"1--%@",[NSThread currentThread]);
    });

    dispatch_async(queueGlobal, ^{
        sleep(2);
        NSLog(@"2--%@",[NSThread currentThread]);
    });

    dispatch_barrier_async(queueGlobal, ^{
        NSLog(@"barrier--%@",[NSThread currentThread]);
    });

    dispatch_async(queueGlobal, ^{
        NSLog(@"3--%@",[NSThread currentThread]);
    });

    dispatch_async(queueGlobal, ^{
        NSLog(@"4--%@",[NSThread currentThread]);
    });
复制代码

全局队列是一个并发队列,盲猜这里应该先打印出来12,然后执行barrier,然后随机打出34,但是实际上呢

image.png

barrier似乎跟dispatch_async一样,并没有起到什么作用,查了一番资料终于在官方文档找到正解
dispatch​_barrier​_async

image.png

如果自己通过dispatch_queue_create创建并发队列没问题,如果是一个串行队列或者全局并发队列那么和dispatch_async效果一样一样的,我们换成自己创建的并发队列

    dispatch_queue_t queueConcurrent = dispatch_queue_create("com.conc", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queueConcurrent, ^{
        NSLog(@"1--%@",[NSThread currentThread]);
    });

    dispatch_async(queueConcurrent, ^{
        sleep(2);
        NSLog(@"2--%@",[NSThread currentThread]);
    });

    dispatch_barrier_async(queueConcurrent, ^{
        NSLog(@"barrier--%@",[NSThread currentThread]);
    });

    dispatch_async(queueConcurrent, ^{
        NSLog(@"3--%@",[NSThread currentThread]);
    });

    dispatch_async(queueConcurrent, ^{
        NSLog(@"4--%@",[NSThread currentThread]);
    });
    NSLog(@"end");
复制代码

image.png

从结果看到dispatch_barrier_async并没有阻塞当前线程,而只是阻塞了当前异步队列其他任务的执行,官方文档也证实了这一点

image.png

dispatch_barrier_sync

我们先去官方文档看一下dispatch_barrier_sync

image.png

一方面dispatch_barrier_sync会阻塞当前线程,另一方面可能造成死锁deadlock,我们验证一下

    dispatch_queue_t queueConcurrent = dispatch_queue_create("com.conc", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queueConcurrent, ^{
        sleep(2);
        NSLog(@"1--%@",[NSThread currentThread]);
    });

    dispatch_async(queueConcurrent, ^{
        NSLog(@"2--%@",[NSThread currentThread]);
    });

    dispatch_barrier_sync(queueConcurrent, ^{
        NSLog(@"barrier--%@",[NSThread currentThread]);
    });

    dispatch_async(queueConcurrent, ^{
        NSLog(@"3--%@",[NSThread currentThread]);
    });

    dispatch_async(queueConcurrent, ^{
        NSLog(@"4--%@",[NSThread currentThread]);
    });
    NSLog(@"end");
复制代码

image.png

我们看到dispatch_barrier_sync确实阻塞了当前线程,end是在dispatch_barrier_sync之后输出的

    dispatch_queue_t queueConcurrent = dispatch_queue_create("com.conc", DISPATCH_QUEUE_CONCURRENT);

    dispatch_barrier_sync(queueConcurrent, ^{
        NSLog(@"barrier--%@",[NSThread currentThread]);
        dispatch_sync(queueConcurrent, ^{
            NSLog(@"%@",[NSThread currentThread]);
        });
    });
复制代码

image.png

不知道这是不是文档中所说的Calling this function and targeting the current queue results in deadlock.反正实现了deadlock效果

另外文档提到了Block_copy

image.png

我从源码看到确实dispatch_barrier_async调用了_dispatch_Block_copy

image.png

image.png

dispatch_barrier_sync是没有copy,这可能是因为dispatch_barrier_sync阻塞线程后面代码不执行,而dispatch_barrier_async没有阻塞线程,那么后面就可能对函数体修改????所以拷贝一份这里留下问好

GCD 延时执行方法:dispatch_after

2秒之后将任务添加到主队列,具体什么时候执行还要看CUP的调度

    dispatch_queue_t mainQueue = dispatch_get_main_queue();

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0*NSEC_PER_SEC)), mainQueue, ^{
       //2秒之后将任务添加到主队列,具体什么时候执行还要看CUP的调度
        NSLog(@"执行了");
    });
复制代码

GCD 一次性代码(只执行一次):dispatch_once

实现单例

static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
       //单例
    });
复制代码

更多内容可以看一下# iOS多线程之dispatch_once剖析

GCD 快速迭代方法:dispatch_apply

先来看一下文档dispatch_apply

image.png

可以看到dispatch_apply和可重入且安全的并发队列可以实现高效的遍历操作,如果是一个串行队列那么就体现不出来他的高效之处了

    dispatch_apply(10, queueGlobal, ^(size_t index) {
        NSLog(@"%zd--%@",index,[NSThread currentThread]);
    });
    NSLog(@"end");
复制代码

image.png

dispatch_apply会利用多个线程来遍历,不光是子线程,还可以调用主线程,另外他还会阻塞当前线程

Dispatch Group

image.png

dispatch_group作为一个单元监控一组任务。你可以将一组任务放到一个组里通过dispatch_group同步他们的行为。你可以将这些任务放在组里以后在同一个队列或者不同的队列异步执行,可以在不阻塞当前线程的情况下监听这些异步任务执行完毕,也可以阻塞当前线程等待这些任务完成。

dispatch_group_notify

    dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);

    dispatch_group_async(group, queueGlobal, ^{
        sleep(2);
        NSLog(@"1--%@",[NSThread currentThread]);
    });

    dispatch_group_async(group, queueGlobal, ^{
        NSLog(@"2--%@",[NSThread currentThread]);
    });

    dispatch_group_async(group, queueGlobal, ^{
        sleep(1);
        NSLog(@"3--%@",[NSThread currentThread]);
    });

    dispatch_group_notify(group, queueGlobal, ^{
        NSLog(@"notify--%@",[NSThread currentThread]);
    });
    NSLog(@"end--%@",[NSThread currentThread]);
复制代码

image.png

dispatch_group_notify并没有阻塞当前线程,他是在当前组的其他队列都执行完成之后再执行,我们可以将dispatch_group_notify任务放在主队列执行,这就实现了回调主线程的功能

    dispatch_queue_t queueSerial = dispatch_queue_create("com.serial", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queueMain = dispatch_get_main_queue();
    dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);

    dispatch_group_async(group, queueGlobal, ^{
        sleep(2);
        NSLog(@"1--%@",[NSThread currentThread]);
    });

    dispatch_group_async(group, queueSerial, ^{
        sleep(1);
        NSLog(@"2--%@",[NSThread currentThread]);
    });

    dispatch_group_async(group, queueSerial, ^{
        NSLog(@"3--%@",[NSThread currentThread]);
    });

    dispatch_group_notify(group, queueMain, ^{
        NSLog(@"notify--%@",[NSThread currentThread]);
    });
    NSLog(@"end--%@",[NSThread currentThread]);
复制代码

image.png

dispatch_group_wait

阻塞当前线程,等待组内任务都执行完成才会继续执行

    dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);

    dispatch_group_async(group, queueGlobal, ^{
        sleep(2);
        NSLog(@"1--%@",[NSThread currentThread]);
    });

    dispatch_group_async(group, queueGlobal, ^{
        sleep(1);
        NSLog(@"2--%@",[NSThread currentThread]);
    });

    dispatch_group_async(group, queueGlobal, ^{
        NSLog(@"3--%@",[NSThread currentThread]);
    });

    dispatch_group_wait(group, DISPATCH_TIME_FOREVER); 
    NSLog(@"end--%@",[NSThread currentThread]);
复制代码

image.png

直到前面三个任务都执行完成才会打印end,当然也可以将阻塞的超时时间设置小一些,即使前面任务没有完成,但是时间到了也会继续执行

    dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);

    dispatch_group_async(group, queueGlobal, ^{
        sleep(3);
        NSLog(@"1--%@",[NSThread currentThread]);
    });

    dispatch_group_async(group, queueGlobal, ^{
        sleep(1);
        NSLog(@"2--%@",[NSThread currentThread]);
    });

    dispatch_group_async(group, queueGlobal, ^{
        NSLog(@"3--%@",[NSThread currentThread]);
    });

    dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2*NSEC_PER_SEC)));
    NSLog(@"end--%@",[NSThread currentThread]);
复制代码

image.png

dispatch_group_enter、dispatch_group_leave

dispatch_group_async内部也是通过dispatch_group_enterdispatch_group_leave来实现的

    dispatch_queue_t queueGlobal = dispatch_get_global_queue(0, 0);

    dispatch_group_enter(group);
    dispatch_async(queueGlobal, ^{
        sleep(3);
        NSLog(@"1--%@",[NSThread currentThread]);
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queueGlobal, ^{
        sleep(1);
        NSLog(@"2--%@",[NSThread currentThread]);
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(queueGlobal, ^{
        NSLog(@"3--%@",[NSThread currentThread]);
        dispatch_group_leave(group);
    });

    dispatch_group_notify(group, queueGlobal, ^{
        NSLog(@"notify--%@",[NSThread currentThread]);
    });

    NSLog(@"end--%@",[NSThread currentThread]);
复制代码

实现了和dispatch_group_async一样的效果
image.png

信号量semaphore

在开发中经常需要线程同步,那么信号量是一个很好的选择,dispatch_semaphore_signal信号量+1,dispatch_semaphore_wait信号量-1,如果信号量小于0那么阻塞当前线程,可以设置阻塞的超时时间

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    dispatch_async(queueGlobal, ^{
        sleep(3);//耗时操作
        NSLog(@"1--%@",[NSThread currentThread]);
        dispatch_semaphore_signal(semaphore);//信号量+1
    });

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//信号量-1
    dispatch_async(queueMain, ^{
        NSLog(@"主线程");
    });
    NSLog(@"end--%@",[NSThread currentThread]);
复制代码

image.png

多线程访问同一个数据有可能造成数据不安全,例如

    __block int i = 5;
    while (i>0) {
        dispatch_async(queueGlobal, ^{
            i--;
            NSLog(@"%d--%@",i,[NSThread currentThread]);
        });
    }
复制代码

image.png

由于多线程同时修改i导致结果和预期出现了很大的出入,甚至NSLog函数的打印都出问题了?,通过信号量可以解决这个问题

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    __block int i = 5;
    
    while (i>0) {
        dispatch_async(queueGlobal, ^{
            i--;
            NSLog(@"%d--%@",i,[NSThread currentThread]);
            dispatch_semaphore_signal(semaphore);
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    }
复制代码

这个时候我们看到结果按照预期输出了,因为信号量控制最多只有一个线程可以访问变量i(不一定是同一个线程,但同一时间最多只能有一个线程访问)

image.png

参考文章

iOS 多线程:『GCD』详尽总结

Dispatch

源码libdispatch

iOS多线程之dispatch_once剖析

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享