美文网首页
iOS 多线程 --- GCD

iOS 多线程 --- GCD

作者: wxhan | 来源:发表于2019-03-11 12:37 被阅读0次

    一.进程&线程

    • 进程:是程序执行过程中分配和管理资源的一个基本单位。
    • 线程:是程序执行过程中任务调度和执行的一个基本单位。
    • 一个进程里面有多个线程,线程是进程的一部分

    二. 任务

    任务:就是要执行什么操作。

    1. 同步执行(sync):
    • 在当前的线程上面执行任务。
    • 不具备开启新线程的能力。
    2. 异步执行(async):
    • 可以开启新的线程执行任务。
    • 具备开启新线程的能力。

    三. 队列

    队列:用于存放任务,遵循FIFO(先进先出)的原则。

    1. 串行队列(serial):
    • 队列中每次只执行一个任务,当第一个执行完才能执行第二个。
    • 只能开启一个线程。
    2. 并发队列(concurrent):
    • 队列中可以同时执行多个任务。
    • 可以开启多个线程。
    3. 两种特殊队列:
    • 全局队列(dispatch_get_global_queue):直接作为普通并发队列使用。
    • 主队列(dispatch_get_main_queue):任务在主线程中执行的串行队列。

    四. 使用步骤

    注:任务放到...(主队列 & 串行队列 & 全局队列)队列中...(同步 & 异步)执行。

    1. 创建队列
    • 串行队列 & 并发队列
    // 串行队列
    dispatch_queue_t serialQueue = dispatch_queue_create("com.wxh.queue", DISPATCH_QUEUE_SERIAL);
    // 并发队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.wxh.queue", DISPATCH_QUEUE_CONCURRENT);
    
    

    注:第一个参数表示队列的唯一标识符DEBUG的时候用的,可以为空,推荐使用类似于APP的BundleID这种逆序域名;第二个参数识别是串行队列还是并发队列串行队列DISPATCH_QUEUE_SERIAL,并发队列用DISPATCH_QUEUE_CONCURRENT

    • 主队列 & 全局队列
    // 主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    // 全局队列
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    

    注:主队列不用传参数。全局队列第一个参数一般用DISPATCH_QUEUE_PRIORITY_DEFAULT,表示优先级的。第二个参数暂时没用,用0即可。

    2. 创建任务
    // 同步任务创建
    dispatch_sync(queue, ^{
    });
    // 异步任务创建
    dispatch_async(queue, ^{
    });
    

    注:参数queue就是队列的类型。

    3. 小结
    • 并发队列全局队列使用场景是一致的,通常都是使用全局队列
    • 总共有3种队列:全局队列主队列串行队列;有2种任务:同步任务异步任务
    • 共有6种使用方式,能否开启新的线程有同步或者 异步决定,但是否开启新的线程要看当前状况是否需要开启新的线程来决定。
    • sync会照成阻塞现象,sync任务下的队列里面的任务要必须完成一个才能继续下一个。
    • sync阻塞的是队列,不是线程。

    五. 使用

    1. 同步主队列 syncMain

    解读:任务放到主队列同步执行

    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"任务1");
        NSLog(@"任务2");
    });
    
    • 如果在主线程中执行syncMain,会出现死锁现象。
      解释:在主线程上面执行同步主队列syncMain任务,相当于把syncMain任务加到了主队列Main中。当要执行任务1的时候,也要先把任务1加到Main中。因为是同步任务sync,所以要执行任务1之前,必须先等待syncMain任务完成。但是要完成syncMain任务前,又必须执行任务1,这时候syncMain任务和任务1之间就会互相等待,出现死锁现象,程序崩溃。

    • 如果是在子线程中使用syncMain,不开启新的线程。先完成任务1,再完成任务2
      解释:因为是同步任务sync,不具备开启新的线程的能力。在子线程中执行syncMain,相当于吧syncMain任务加到了子线程的队列(这里用队列A表示)中。当要执行任务1任务2的时候,把任务1任务2加到主队列Main中,这个时候,Main中是没有其他任务的,所以不会出现死锁现象。Main是特殊的串行队列,所以先执行完任务1,再执行任务2

    2. 异步主队列 asyncMain

    解读:任务放到主队列异步执行

    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"任务1");
        NSLog(@"任务2");
    });
    

    不开启新的线程。先完成任务1,再完成任务2
    解释:虽然是异步任务async,具备开启新的线程的能力。但是由于是在主队列Main中执行任务,Main中的任务必须在主线程完成,所以不需要开启新的线程。在主线程中执行asyncMain任务,相当于把asyncMain任务加到主队列Main中。当要执行任务1的时候,也要任务1加到Main中。因为是异步任务async,所以asyncMain任务可以先等待,先执行完任务1,再执行任务2,所以不会出现死锁现象;在子线程执行asyncMain任务,跟子线程执行syncMain任务同等逻辑。

    使用场景:做网络请求从后台接口获取到数据之后,需要根据数据更新界面UI,一般都是用asyncMain,在asyncMainblock里面执行刷新界面的操作。

    3.同步串行队列 syncSerial

    解读:任务放到串行队列同步执行

    dispatch_sync(dispatch_queue_create("com.wxh.queue", DISPATCH_QUEUE_SERIAL), ^{
        NSLog(@"任务1");
        NSLog(@"任务2");
    });
    

    不会开启新的线程。先完成任务1,再完成任务2
    解释:因为是同步任务syncsync不具备开启新的线程。执行syncSerial任务时,syncSerial任务存放在当前线程的队列(这里用队列B表示)中,执行任务1的时候,将任务1放到当前线程的串行队列Serial中,任务2也一样。因为是Serial,所以要先执行完任务1,再执行任务2

    问题:同步串行队列syncSerial在主线程上执行,为什么不会出现死锁现象?
    回答:在主线程上执行syncSerial任务,syncSerial任务存放在主队列Main当中,而任务1任务2都放在主线程的串行队列Serial中(不是在主队列Main中哦~)。此时,主线程上面有两个队列,一个是存放syncSerial任务的Main,另一个是存放任务1任务2Serial。当执行syncSerial任务中的任务1时,会从主队列Main去到串行队列Serial,然后在Serial继续执行任务2,执行完任务2,回到主队列Main中完成syncSerial任务。

    4.异步串行队列 asyncSerial

    解读:任务放到串行队列异步执行

    dispatch_async(dispatch_queue_create("com.wxh.queue", DISPATCH_QUEUE_SERIAL), ^{
        NSLog(@"任务1");
        NSLog(@"任务2");
    });
    

    会开启一条新的线程。先完成任务1,再完成任务2
    解释:因为是异步任务async,具备开启新的线程的能力。因为是在串行队列Serial中,只能同时执行一个任务,所以只需要开启一条新的线程。asyncSerial任务存放在当前线程的队列中(这里用队列C表示),而任务1任务2存放在新开启的线程的Serial。当执行asyncSerial任务,要开始执行任务1时,先去到新开启的线程的Serial中,执行完任务1,再执行任务2,然后回到队列C中完成asyncSerial任务。

    5. 同步全局队列 syncGlobal

    解读:任务放到全局队列同步执行

    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"任务1");
        NSLog(@"任务2");
    });
    

    不会开启新的线程。先完成任务1,再完成任务2
    解释:因为是同步任务sync,不具备开启新的线程能力。虽然是在全局队列Global中,可以多个任务同时进行,但是只有一条线程,所以还是要先完成任务1再执行任务2syncGlobal任务存放在当前线程的队列(这里用队列D表示)中,执行任务1的时候,将任务1放到当前线程的全局队列Global中,任务2也一样。虽然是Global,但是只有一条线程,所以要先执行完任务1,再执行任务2

    使用场景:上传多张图片到后台,后台要求一张一张的上传。

    6. 异步全局队列 asyncGlobal

    解读:任务放到全局队列异步执行

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"任务1");
        NSLog(@"任务2");
    });
    

    会开启多条新的线程。任务1任务2同时执行。
    解释:因为是异步任务asyncasync会具备开启新的线程能力。因为是全局队列Global,所以Global里面的任务可以同时执行。asyncGlobal任务存放在当前线程的队列(这里用队列E表示)中,而任务1任务2存放在各自开启的线程队列中。当执行asyncGlobal任务,因为是Global,所以任务1任务2可以同时执行。

    使用场景:上传多张图片到后台,可以多张同时上传。

    六.GCD线程之间的通信

    异步开启子线程执行耗时任务,耗时任务完成,利用主队列回到主线程更新UI。

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            for (int i = 0; i < 10; i ++) {
                NSLog(@"---%d",i);
                // 模拟耗时操作
                [NSThread sleepForTimeInterval:1];
            }
        });
        dispatch_async(dispatch_get_main_queue(), ^{
            // 耗时操作完成
            NSLog(@"任务完成,回到主线程更新UI。");
        });
    });
    

    七.阻塞方法dispatch_barrier

    作用:在有多个任务并且使用栅栏方法dispatch_barrier的队列(注意:不能使用全局队列),必须先等待dispatch_barrier前面的任务执行完毕,才能执行dispatch_barrier里面的任务。等待dispatch_barrier里面的任务执行完毕,才能继续执行dispatch_barrier之后的任务。
    例子:有三种图片,分别压缩之后,一起上传后后台。

    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.wxh.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"压缩图片1");
    });
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"压缩图片2");
    });
    dispatch_async(concurrentQueue, ^{
        [NSThread sleepForTimeInterval:4];
        NSLog(@"压缩图片3");
    });
    dispatch_barrier_async(concurrentQueue, ^{
        NSLog(@"将 压缩图片1 压缩图片2 压缩图片3 上传到后台");
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"其他操作");
    });
    

    结果:这样子既能让3张图片同时压缩,又能确保3张图片都压缩完之后,才将3张图片上传到后台。

    问题1:为什么不能使用 全局队列
    解释:苹果官方给的说明是如果使用全局队列,那么dispatch_barrier_async方法将退化成dispatch_async方法。个人觉得,不知道对不对,全局队列没有名字,自定义的并发队列是有名字的,系统需要重新控制队列里面任务的执行操作,必须具体到哪个队列中去重新控制。

    问题2:dispatch_barrier_asyndispatch_barrier_syn的区别?
    解释:上例中,将dispatch_barrier_asyn替换成dispatch_barrier_syn效果是一样的。它们的区别在于,

    • dispatch_barrier_asyn将自己的任务加入到队列中之后,不用等自己的任务执行完毕,它就将它后面的任务也加入到队列中。然后等待自己的任务执行完毕,才执行后面的任务。
    • dispatch_barrier_syn将自己的任务加入到队列中之后,需要等待自己的任务执行完毕,才能加入后台的任务,并执行后面的任务。

    八.延时方法 dispatch_after

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    });
    

    方法中需要传入一个延时的时间(秒),延时操作里面的任务放到主队列执行。

    九.只执行一次(单例) dispatch_once

    + (instancetype)shareInstance{
        static Singleton *single;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            single = [[Singleton alloc] init];
        });
        return single;
    }
    

    在程序运行过程中,dispatch_once方法中的代码只会被执行1次,即不影响性能,又能保证线程安全。

    • 原理:dispatch_once方法是根据dispatch_once_t修饰的变量onceToken的值来决定接下来的操作的。
      (1)当onceToken = 0时,说明程序第一次执行dispatch_once方法,直接执行dispatch_onceblock中的代码。
      (2)当onceToken = -1时,说明程序已经执行完过dispatch_once方法,那么跳过dispatch_onceblock的代码,执行block之后的代码。
      (3)当onceToken != 0onceToken != -1时,说明现在有线程(这里用线程A表示)在执行dispatch_once方法,但是还没执行完毕。这个时候,当前这条线程处于阻塞状态,等待线程A执行完毕。当线程A执行完dispatch_once方法时,onceToken的值会变成-1,这时候当前这条线程继续执行。
      (4)单例可以看成是一种特殊的实例,是一个全局的对象,有且只有一个的对象。

    十.快速迭代 dispatch_apply

    NSLog(@"---");
    dispatch_apply(10, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) {
        // 模拟耗时操作
        [NSThread sleepForTimeInterval:(11 - index)];
        NSLog(@"%zu",index);
    });
    NSLog(@"+++");
    
     ---
     3
     2
     1
     0
     6
     5
     4
     7
     9
     8
     +++
    

    dispatch_apply是一个快速迭代的方法,类似于for循环

    • 如果方法中传入的是一个串行队列,那么dispatch_apply里面的耗时操作就需要按顺序同步执行(相当于异步串行队列,必须执行完成一个任务之后,才能执行下一个任务。不过一般不会这么做,这样操作就失去了快速迭代的意义)。
    • 如果方法中传入的是一个全局队列,那么里面多个耗时操作就可以同时进行(相当于异步并发队列,可以多个任务同时执行)。
    • 无论传入的是串行队列还是全局队列dispatch_apply方法都会阻塞当前线程等待所有任务执行完毕,才能执行dispatch_apply方法后面的代码(相当于同步任务)。

    十一. 队列组 dispatch_group

    需求:在填写个人资料页面,我们需要把个人的信息(名字,手机号等)上传到后台,也需要把照片(身份证正反面拍照等)也上传到后台,需要做两个网络请求。当两个网络请求都成功回调之后,返回上一个页面。

    1. 第一种方法:使用dispatch_group_notify监听。

    NSLog(@"---");
    dispatch_group_t group =  dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:3];
            NSLog(@"上传个人信息");
        }
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i = 0; i < 3; ++i) {
            [NSThread sleepForTimeInterval:1];
            NSLog(@"上传图片资料");
        }
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"返回上个界面");
    });
    NSLog(@"===");
    
    ---
    ===
    上传图片资料
    上传图片资料
    上传个人信息
    上传图片资料
    上传个人信息
    返回上个界面
    
    • dispatch_group_create()创建一个队列组。
    • dispatch_group_async将任务放到队列里面,然后再讲队列放到队列组里面。
    • dispatch_group_notify监听队列组中其他队列的任务完成状态,当所有的任务都执行完成之后,将自身block里面的任务也方法队列组中,执行任务。
    • dispatch_group_notify不会阻塞当前线程。

    2. 第二种方法:使用dispatch_group_wait阻塞当前线程。

    NSLog(@"---");
    dispatch_group_t group =  dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:3];
            NSLog(@"上传个人信息");
        }
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i = 0; i < 3; ++i) {
            [NSThread sleepForTimeInterval:1];
            NSLog(@"上传图片资料");
        }
    });
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"返回上个界面");
    NSLog(@"===");
    
    ---
    上传图片资料
    上传图片资料
    上传个人信息
    上传图片资料
    上传个人信息
    返回上个界面
    ===
    
    • 当所有任务都完成之后,才执行dispatch_group_wait后面的任务。
    • dispatch_group_wait会阻塞当前的线程。

    3. 第三种方法:使用dispatch_group_enterdispatch_group_leave组合代替dispatch_group_async

    NSLog(@"---");
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:3];
            NSLog(@"上传个人信息");
        }
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        for (int i = 0; i < 3; ++i) {
            [NSThread sleepForTimeInterval:1];
            NSLog(@"上传图片资料");
        }
        dispatch_group_leave(group);
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"返回上个界面");
    });
    NSLog(@"===");
    
    ---
    ===
    上传图片资料
    上传图片资料
    上传个人信息
    上传图片资料
    上传个人信息
    返回上个界面
    
    • dispatch_group_enter表示把一个任务放到队列组group,并开始执行。
    • dispatch_group_leave 表示任务执行完毕。
    • dispatch_group_enter + dispatch_group_leave = dispatch_group_async
    • 可以用dispatch_group_notify,同样也可以用dispatch_group_wait(结果跟第2种方法一样)。区别是后者会照成当前线程阻塞,前者不会。
    • group中的所有任务都执行完毕时,才会执行dispatch_group_wait后面的任务,或者执行追加到dispatch_group_notify中的任务。

    十二. 信号量 dispatch_semaphore

    三个重要方法

    • dispatch_semaphore_create:创建并初始化一个信号总量,一般为0或者1
    • dispatch_semaphore_signal:发送一个信号,即让信号总量+1
    • dispatch_semaphore_wait:如果当前信号总量为0,那么阻塞当前线程,否则。信号总量-1,正常执行。

    1. 异步线程变成同步。

    需求:有时候需要实时拿到异步里面耗时操作的结果,才能正确的执行之后的代码。

    __block NSInteger i = 1;
    // 创建一个信号总量为`0`的信号量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [NSThread sleepForTimeInterval:2];
        i ++;
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"%ld",(long)i);
    

    打印结果为:2
    解释:当第一次执行dispatch_semaphore_wait时,信号总量为0,当前线程阻塞。当执行完异步block里面的耗时操作之后,执行了dispatch_semaphore_signal,信号总量+1。从block里面出来第二次执行dispatch_semaphore_wait时,信号总量为1,正常执行。这样就能等到异步执行完之后,再执行接下来的代码(类似于同步执行)。

    2. 线程安全(线程锁)

    需求:有时候,我们会在多个地方同时对同一个接口进行调用,那如果每次调用过程会对下一次调用的结果有影响(有修改或者更变等操作),那么我们就必须保证该接口同一时间只能被一个地方调用,这就是线程安全

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.semaphore = dispatch_semaphore_create(1);
    }
    - (void)tiaoyongjiekou{
        dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // 模拟耗时操作
            [NSThread sleepForTimeInterval:1.0];
            dispatch_semaphore_signal(weakSelf.semaphore);
        });
    }
    

    解释:在外部创建一个信号总量为1的信号量,当第一次调用tiaoyongjiekou方法,执行到dispatch_semaphore_wait时,因为当前信号总量为1,那么正常执行并且信号总量-1(此时信号总量为0)。如果第一次调用还没执行完成,第二次就开始调用,当执行到dispatch_semaphore_wait时,信号总量为0,线程阻塞,只能原地等待。等第一次调用结束,执行完耗时操作之后,执行了dispatch_semaphore_signal,信号总量+1(此时信号总量为1),那么第二次调用才能继续执行。这样就能确保同一时间只被调用一次,确保线安全。

    相关文章

      网友评论

          本文标题:iOS 多线程 --- GCD

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