iOS GCD

688 阅读11分钟

串行并行、同步异步

一、概念
  • 同步执行(sync): 在当前线程中执行任务,任务未执行完时,会阻塞线程,不会开辟线程。
  • 异步执行(async): 另开辟线程执行任务,不会阻塞当前线程。
  • 串行队列: 按顺序一个一个执行,FIFO先进先出原则
  • 并行队列: 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

一般使用中会有如下四种情况:

  • 串行队列,同步执行: 在当前线程中,一个个执行
  • 串行队列,异步执行: 另开辟一个线程,一个个执行
  • 并行队列,同步执行: 在当前线程中,一个个执行
  • 并行队列,异步执行: 另开辟多个线程,同时执行

也就是说串行同步和并行同步作用是一样的。

串行同步:
/**
 串行队列,同步执行
 */
- (void)createSerialQueueWithSync {
    dispatch_queue_t serialQueue = dispatch_queue_create("com.neusoft", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serialQueue, ^{
        NSLog(@"A==%@", [NSThread currentThread]);
    dispatch_sync(serialQueue, ^{
        NSLog(@"B==%@", [NSThread currentThread]);
    });
    dispatch_sync(serialQueue, ^{
        NSLog(@"C==%@", [NSThread currentThread]);
    });
    NSLog(@"D==%@", [NSThread currentThread]);
}

串行同步打印结果如下:
image.png

从结果中我们能看到,符合串行队列的特征(一个一个执行),符合同步执行特征(在当前线程thread number == 1执行,并且阻塞了当前线程)。

串行异步:
/**
 串行队列,异步执行
 */
- (void)createSerialQueueWithAsync {
    dispatch_queue_t serialQueue = dispatch_queue_create("com.neusoft", DISPATCH_QUEUE_SERIAL);
    dispatch_async(serialQueue, ^{
        NSLog(@"A==%@", [NSThread currentThread]);
    });
    dispatch_async(serialQueue, ^{
        NSLog(@"B==%@", [NSThread currentThread]);
    });
    dispatch_async(serialQueue, ^{
        NSLog(@"C==%@", [NSThread currentThread]);
    });
    NSLog(@"D==%@", [NSThread currentThread]);
}

串行异步打印结果如下

image.png

从结果中可以看到,符合串行队列的特征(一个一个执行),符合异步执行特征(另开辟线程->number == 3,且不阻塞当前线程)。

并行同步
/**
 并行队列,同步执行
 */
- (void)createConcurrentQueueWithSync {
    dispatch_queue_t conCurrentQueue = dispatch_queue_create("com.neusoft", DISPATCH_QUEUE_CONCURRENT);
    //    dispatch_queue_t conCurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//这个是全局并行队列,一般并行任务都会加到这里面去
    
    dispatch_sync(conCurrentQueue, ^{
        NSLog(@"A==%@", [NSThread currentThread]);
    });
    dispatch_sync(conCurrentQueue, ^{
        NSLog(@"B==%@", [NSThread currentThread]);
    });
    dispatch_sync(conCurrentQueue, ^{
        NSLog(@"C==%@", [NSThread currentThread]);
    });
    NSLog(@"D==%@", [NSThread currentThread]);
}

并行同步打印结果如下

image.png

从结果可以看出,符合同步特征(在当前线程,且阻塞当前线程),但是并不清楚是否符合并行队列的特征,并行队列的效果需要异步执行才能看出来,并行同步整体的效果和串行同步相同。

并行异步
/**
 并行队列,异步执行
 */
- (void)createConcurrentQueueWithAsync {
    dispatch_queue_t conCurrentQueue = dispatch_queue_create("com.neusoft", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"A==%@", [NSThread currentThread]);
    });
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"B==%@", [NSThread currentThread]);
    });
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"C==%@", [NSThread currentThread]);
    });
    NSLog(@"D==%@", [NSThread currentThread]);
}

并行异步打印结果如下 image.png
从结果中可以看出,符合并行队列特征(可以并发执行线程任务),符合异步执行特征(另开辟线程,且不阻塞当前线程)。

GCD中的队列有三种:串行队列、并行队列、主队列
串行队列:
dispatch_queue_t queue1 = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);

第一个参数是队列名称,第二个是一个宏定义,常用的两个宏 DISPATCH_QUEUE_SERIALDISPATCH_QUEUE_CONCURRENT 分别表示串行队列和并行队列。

并发队列: 全局并发队列 和 手动创建并发队列

全局并发队列创建:

/*  - QOS_CLASS_USER_INTERACTIVE
 *  - QOS_CLASS_USER_INITIATED
 *  - QOS_CLASS_DEFAULT
 *  - QOS_CLASS_UTILITY
 *  - QOS_CLASS_BACKGROUND
*/
//dispatch_get_global_queue(intptr_t identifier, uintptr_t flags); 

dispatch_queue_t queue = dispatch_get_global_queue(0,0);

手动创建并发队列:

// 串行执行,第一个参数是名称 ,第二个是标识:DISPATCH_QUEUE_CONCURRENT,并发队列标识
dispatch_queue_t queue = dispatch_queue_create("myQueue",DISPATCH_QUEUE_CONCURRENT);

主队列
dispatch_queue_t queue = dispatch_get_main_queue();
主队列和全局并发队列
  • 主队列: 是和主线程相关联的队列,主队列是GCD自带的一种特殊的串行队列,放在主队列中得任务,都会放到主线程中执行。 ( 提示:如果把任务放到主队列中进行处理,那么不论处理函数是异步的还是同步的都不会开启新的线程。 都在主线程执行)
  • 全局并发队列: 会自动创建子线程来执行任务,这个队列的优先级可以设置为高、默认、低或后台。 全局并发队列的特点:
    • ‌共享性‌:全局并发队列可以被所有的应用程序共享。
    • ‌并发执行‌:多个任务可以在不同的子线程上并发执行。
    • ‌调度方式‌:GCD默认已经提供了全局的并发队列,供整个应用使用,不需要手动创建。
//全局并发队列
dispatch_get_global_queue
//主线程--串行队列
dispatch_get_main_queue()

这两个队列在系统刚启动时就创建好的队列,一般不建议使用全局并发队列,因为在全局并发队列中也会执行系统的任务,对于调试和业务剥离等造成影响。
死锁

死锁总结一句话:串行队列(当然也包括主队列)中向该队列添加同步任务,必定导致死锁。
两个死锁案例帮助理解这句话:
例 1:

- (void)test1 {
    //1,5,2,造成死锁,
    //1,5,2,async是异步,所以会开辟线程,当然开辟线程需要耗时,所以5在先,2在后
    //为什么会造成死锁?因为队列中加入任务的顺序是2、4、3,按照队列的FIFO原则,理应4执行完再执行3,但执行完2遇到了sync同步函数,需要阻塞线程,4需要等待3执行完再执行,造成了互相等待即死锁。
    NSLog(@"1==%@", [NSThread currentThread]);
    dispatch_queue_t queue = dispatch_queue_create("com.serial", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"2==%@", [NSThread currentThread]);
        dispatch_sync(queue, ^{
            NSLog(@"3==%@", [NSThread currentThread]);
        });
        NSLog(@"4==%@", [NSThread currentThread]);
    });
    NSLog(@"5==%@", [NSThread currentThread]);
}

输出结果是 1、5、2,然后发生死锁崩溃,执行过程如下

1、主队列按顺序先执行 test 代码块简称 A,由于dispatch_async(queue是异步串行的另一个队列,不阻塞当前队列,所以这里先执行了 A 的 1 和 5。
2、来到dispatch_async(queue代码块,另起了新的线程处理队列 queue,已知 queue 里面是(2、dispatch_sync(queue、4)代码块简称 B。
3、执行 B 的 2
4、执行dispatch_sync(queue时发现这是同步函数,需要阻塞队列 queue,在当前线程执行dispatch_sync(queue代码块,但这个代码块的要在 queue 中执行,而 queue 已经被阻塞了,造成了互相等待,导致死锁。

例 2:

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"deadlock");
    });
}

同步对于任务是立即执行的,当把任务放进主队列时,它会立即执行,只有执行完这个任务,viewDidLoad才会继续执行。
而viewDidLoad和任务都在主队列上,根据队列的先入先出原则,需要先执行完viewDidLoad再执行任务,造成了互相等待,导致死锁。

二、栅栏函数 barrier

1、概念
在dispatch_barrier_async加入到自定义的并行队列(不能是全局global队列)时,程序会先执行队列中barrier之前的任务,再执行barrier的任务,最后再执行队列中barrier之后的任务。

2、实例
直接上代码
实例一、应用了dispatch_barrier_async的队列中,线程的执行顺序。

- (void)createBarrierAsync {
    dispatch_queue_t queue = dispatch_queue_create("branpeng", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"start==%@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"A==%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"B==%@", [NSThread currentThread]);
    });
    dispatch_barrier_async(queue, ^{
        NSLog(@"barrier_async==%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"C==%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"D==%@", [NSThread currentThread]);
    });
    NSLog(@"end==%@", [NSThread currentThread]);
}

控制台打印效果如下 image.png 从打印结果我们可以看到,程序会先执行队列中barrier之前的任务,再执行barrier的任务,最后再执行队列中barrier之后的任务。

实例二、从网络加载图片,并给图片加上水印后显示到屏幕上

- (void)testPic {
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.waterImage", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(concurrentQueue, ^{
        NSString *logoStr = @"https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3351002169,4211425181&fm=27&gp=0.jpg";
        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr]];
        UIImage *image = [UIImage imageWithData:data];
        [self.mArray addObject:image];
    });
    
    dispatch_async(concurrentQueue, ^{
        NSString *logoStr = @"https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3033952616,135704646&fm=27&gp=0.jpg";
        NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr]];
        UIImage *image = [UIImage imageWithData:data];
        [self.mArray addObject:image];
    });
    
    __block UIImage *newImage = nil;
    dispatch_barrier_async(concurrentQueue, ^{
        
        for (int i = 0; i<self.mArray.count; i++) {
            UIImage *waterImage = self.mArray[i];
            newImage = [LFPrintImageTool printText:@"小姑娘还不睡呀?" onImage:waterImage];
        }
    });
    
    
    dispatch_async(concurrentQueue, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = newImage;
        });
    });
}

先将图片从网络获取下来,才能给它加水印,最后显示到屏幕上,栅栏函数相当于阻塞了当前线程。 注意:栅栏函数必须是自定义的并发队列才有效,且必须是同一队列中的线程才有效。

三、调度组 dispatch_group_t

1、概念
在调度组内的线程都执行完毕后,dispatch_notify函数会触发回调。

2、实例
应用场景:
一个业务需要开启N个异步线程,但是后续操作,需要依赖N个线程返回的数据完成操作。 直接上代码

实例一

- (void)createGroupQueue {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_async(group, queue, ^{
        NSLog(@"A---%@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"B---%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"C---%@", [NSThread currentThread]);
    });
    
    dispatch_notify(group, queue, ^{
        NSLog(@"队列组任务执行完毕");
    });
}

调度组执行结果如下

image.png 从结果中可以看出,符合预期,调度组内的线程都执行完毕后,dispatch_notify函数会触发回调。

实例二

- (void)test2 {
    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, ^{
        NSLog(@"A---%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"B---%@", [NSThread currentThread]);
        }
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"C---%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    dispatch_notify(group, queue, ^{
        NSLog(@"队列组任务执行完毕");
    });
}

上面这个实例是以进组出组的方式实现调度。
dispatch_group_enter(group);
dispatch_group_leave(group);
进组出组的方式类似于信号量,内部有一个signal,enter加1,leave减1,它们总是成对出现,当signal为0时,表示调度组里面的任务都执行完了。

四、信号量 dispatch_semaphore_t

1、作用
可以控制并发队列的最大并发数,当创建的信号量限制为1时,可以达到锁的效果。

2、概念
信号量,一般用来线程并发数量,信号量为几,线程最大并发数就是几

//创建信号量,参数:信号量的初值,当信号量小于0时阻塞当前线程 dispatch_semaphore_create(信号量值)

//等待降低信号量 dispatch_semaphore_wait(信号量,等待时间)

//提高信号量 dispatch_semaphore_signal(信号量)

三、实例

- (void)createSemaphoreQueue {
    //create的value表示,最多几个资源可访问
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
    dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //任务1
    dispatch_async(quene, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"run task 1");
        sleep(1);
        NSLog(@"complete task 1");
        dispatch_semaphore_signal(semaphore);
    });
    //任务2
    dispatch_async(quene, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"run task 2");
        sleep(1);
        NSLog(@"complete task 2");
        dispatch_semaphore_signal(semaphore);
    });
    //任务3
    dispatch_async(quene, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"run task 3");
        sleep(1);
        NSLog(@"complete task 3");
        dispatch_semaphore_signal(semaphore);
    });
}

控制台打印效果如下

image.png 再次执行效果如下

image.png

可以看到,始终都是task1和task2先执行完,信号量经过dispatch_semaphore_signal增加以后,task3才能执行,结论就是不论并发队列中有多少任务等待执行,同一时间只允许两个任务执行。

五、dispatch_after 延迟执行

dispatch_after() 函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到队列中。
这个时间并不是绝对准确的。

 dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3*NSEC_PER_SEC);
 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 dispatch_after(time, queue, ^{
        NSLog(@"3秒后添加到队列");
 });

#注意:相比于GCD方式,performSelector:withObject:afterDelay:有可能不会被调用到
比如:
 dispatch_queue_t t = dispatch_queue_create("这是并发队列", DISPATCH_QUEUE_CONCURRENT);
 dispatch_async(t, ^{
    //这个selector不会执行,因为线程中没有runloop,只有主线程会默认创建runloop
    [self performSelector:@selector(testDelay) withObject:nil afterDelay:3];
    // 解决方法:开启当前线程的runloop  
    //  [[NSRunLoop currentRunLoop] run];
 });
六、dispatch_once 只执行一次

dispatch_once(): 函数能保证某段代码在程序运行过程中只被执行1次,重复调用也没办法重复执行(单例模式中常用此方法)并且即使在多线程的环境下,dispatch_once也可以保证线程安全。

- (void)once {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只执行1次的代码(这里面默认是线程安全的)
    });
}
七、dispatch_apply 重复执行

dispatch_apply() 按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
如果是在串行队列中使用dispatch_apply,那么就和 for 循环一样,按顺序同步执行。
为了不阻塞线程可以使用并发队列dispatch_async来达到异步执行的目的。

还有一点,无论是在串行队列,还是并行队列中,dispatch_apply都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait方法。

 dispatch_queue_t disqueue =  dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
 dispatch_group_t disgroup = dispatch_group_create();

 dispatch_group_async(disgroup, disqueue, ^{
        NSLog(@"1");
 });
    
 dispatch_group_async(disgroup, disqueue, ^{
         NSLog(@"2");
 });
    
 dispatch_group_notify(disgroup, disqueue, ^{
     NSLog(@"notify");
 });
    
# 打印顺序: 1、2、notify