iOS 多线程 --- GCD

2019-03-11  本文已影响0人  wxhan

一.进程&线程

二. 任务

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

1. 同步执行(sync):
2. 异步执行(async):

三. 队列

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

1. 串行队列(serial):
2. 并发队列(concurrent):
3. 两种特殊队列:

四. 使用步骤

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

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. 小结

五. 使用

1. 同步主队列 syncMain

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

dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"任务1");
    NSLog(@"任务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_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_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_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(@"===");
---
===
上传图片资料
上传图片资料
上传个人信息
上传图片资料
上传个人信息
返回上个界面

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(@"===");
---
上传图片资料
上传图片资料
上传个人信息
上传图片资料
上传个人信息
返回上个界面
===

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_semaphore

三个重要方法

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),那么第二次调用才能继续执行。这样就能确保同一时间只被调用一次,确保线安全。

上一篇下一篇

猜你喜欢

热点阅读