GCD 相关一些API探究

403 阅读26分钟

1.GCD是什么

GCD(Grand Central Dispatch)是一种当前较为主流的多线程技术,旨在替代NSThread,充分利用了硬件设备的多核特点,底层由C实现。

2.什么是线程,和进程的关系是什么

OS X或iOS中的每个进程(应用程序)都由一个或多个线程组成,每个线程代表着应用程序代码的单一执行路径。每个应用程序都从一个主线程开始,该线程运行应用程序的main函数。应用程序可以生成其他的子线程,每个子线程执行特定函数的代码。

这里我把进程、子线程、主线程的关系放入这样一个场景:玩一款剧情类的游戏(进程),游戏中有主线任务(主线程)和若干个支线任务(子线程),你可以为了快速通关而只做主线任务,但是你的装备和等级都不够,你就需要去探索一些支线任务打怪升级做装备,但是最终你都要回到主线任务上来才能把这个游戏通关。当然过多探索支线任务会让你付出更多游戏时间(线程开销),扯远了。

3.什么是队列(Dispatch Queue

官方文档:
An object that manages the execution of tasks serially or concurrently on your app's main thread or on a background thread.
在你app主线程或者后台线程上,管理串行或者并发执行任务的对象

感觉很拗口,事实上它是指执行任务处理的等待队列,因为GCD往往搭配block实现(例如:dispatch_async等),block中记录想要执行的任务,并将其追加到Dispatch Queue中,Dispatch Queue遵循FIFO原则执行处理。

官方概述:
Dispatch queues are FIFO queues to which your application can submit tasks in the form of block objects. Dispatch queues execute tasks either serially or concurrently. Work submitted to dispatch queues executes on a pool of threads managed by the system. Except for the dispatch queue representing your app's main thread, the system makes no guarantees about which thread it uses to execute a task.
Dispatch QueueFIFO队列,应用程序可以以block对象的形式向其提交任务。Dispatch Queue以串行或并发方式执行任务。提交给Dispatch Queue的任务在系统维护的线程池中执行。除了代表应用程序主线程的Dispatch Queue外,系统不确定它使用哪个线程执行任务[1]

这里有两个概念:串行队列Serial Dispatch Queue并行队列Concurrent Dispatch Queue[2]
在此对并行队列做一个比喻以更好理解,想象一个场景:
有6批货(6个任务)需要在流水线上制作,由于考虑损耗和时间等因素,厂长决定只开启3条流水线(开启3条线程),某一个流水线上的货制作完成,就继续制作剩下的货,A流水线首先完成制作第1批货,立即开始制作第4批货,1分钟后B流水线完成制作,开始制作第5批货,这个时候C流水线也完成任务,厂长立即关闭了流水线C,然后A制作完第4批货,开始制作第6批货,最后等B把第5批货制作完成,关闭B,最后关闭A,关灯下班。这个这就是往并行队列添加6个任务的模型。

流水线A 流水线B 流水线C
第1批货 第2批货 第3批货
第4批货 第5批货 关闭
第6批货 关闭
关闭

4.GCD相关APIs

在明白线程、队列的概念后,再来探究一些GCD中常用API的用法和场景:
1.dispatch_queue_create
2.dispatch_async 和 dispatch_sync
3.Main Dispatch Queue 和 Global Dispatch Queue
4.dispatch_set_target_queue
5.dispatch_after
6.Dispatch Group
7.dispatch_barrier_async
8.dispatch_apply
9.dispatch_suspend 和 dispatch_resume
10.Dispatch Semaphore
11.dispatch_once
12.Dispatch I/O

4.1 dispatch_queue_create

这是生成Dispatch Queue的方法之一,可以创建串行队列和并行队列,包含两个参数labelattr

/*参数:
label:指定了队列的名称,方便调试,命名规则官方建议reverse-DNS命名风格(域名倒序),可以为NULL,但是定位问题时可能就会遇到麻烦
attr:当你创建串行队列时,推荐传入NULL,但是如果创建并行队列,需要传入DISPATCH_QUEUE_CONCURRENT这个宏
*/
//创建一个串行队列 serialDispatchQueue
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.myserialqueue", NULL);
//创建一个并行队列 concurrentQueue
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.myconcurrentqueue", DISPATCH_QUEUE_CONCURRENT);

说明:

  • 使用dispatch_queue_create方法可以生成任意多的Dispatch Queue,因为系统对于一个串行队列Serial Dispatch Queue只生成并使用一个线程,所以创建多少串行队列,就代表着开辟了多少线程,而过多使用多线程会消耗大量内存,降低性能
  • 同一个串行队列上的任务按照FIFO顺序执行,但是不同串行队列上的任务可以相互并发执行,互不影响
  • 非ARC环境下,当不再需要创建的队列时,应该使用dispatch_release释放。提交到队列的任何block中任务都会对该队列保持引用,因此队列中任务执行完毕之前,不会释放队列。

4.2 dispatch_async 和 dispatch_sync

dispatch_async,这个函数的async代表着“异步”(asynchronous),意味着它总是在block中任务提交后立即返回,不等待block被调用

dispatch_async(serialQueue, ^{
    NSLog(@"串行队列serialQueue中添加的任务1");
});
dispatch_async(serialQueue, ^{
    NSLog(@"串行队列serialQueue中添加的任务2");
});

dispatch_async(concurrentQueue, ^{
    NSLog(@"并行队列concurrentQueue中添加的任务1");
});
dispatch_async(concurrentQueue, ^{
    NSLog(@"并行队列concurrentQueue中添加的任务2");
});

这里,串行队列serialQueue上的任务执行顺序是先1后2,并行队列concurrentQueue上的执行顺序不确定

dispatch_sync这个函数和上面的恰恰相反,意思就是“同步”(synchronous),也就是说将指定block同步追加到队列中,在block执行结束之前,dispatch_sync函数会一直等待,等待,意味着当前线程停止,所以在主队列Main Dispatch Queue即主线程上调用这个函数的时候,追加到主队列中的任务不会被执行,这个时候就发生了死锁
当并行队列中的各个任务之间需要界定先后顺序,比如:任务2需要任务1执行的结果作为参数,可以使用dispatch_sync

dispatch_sync(concurrentQueue, ^{
    NSLog(@"任务1");
});
dispatch_sync(concurrentQueue, ^{
    NSLog(@"任务1完成后,进行任务2");
});
dispatch_sync(concurrentQueue, ^{
    NSLog(@"任务1和2都完成后,进行任务3");
});
/*
运行结果:
任务1
任务1完成后,进行任务2
任务1和2都完成后,进行任务3
*/

4.3 Main Dispatch Queue 和 Global Dispatch Queue

Main Dispatch Queue是系统提供的主线程中执行的专属队列,自然,它属于串行队列,追加到Main Dispatch Queue的任务在主线程的Runloop中执行,一般都是UI更新等必须在主线程中执行的操作

dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue, ^{
    NSLog(@"主队列mainQueue添加的任务");
});

Global Dispatch Queue是所有应用程序都能使用的并行队列,所以不必要每次通过dispatch_queue_create去生成队列,直接获取即可。 Global Dispatch Queue有4种优先级(由高到低):
DISPATCH_QUEUE_PRIORITY_HIGH(高)
DISPATCH_QUEUE_PRIORITY_DEFAULT(默认)
DISPATCH_QUEUE_PRIORITY_LOW(低)
DISPATCH_QUEUE_PRIORITY_BACKGROUND(后台)
这里说明一点,这个队列优先级是通过内核管理作用于队列的线程(归根结底是作用于线程)

/*
参数:
identifier:4种long型的宏,代表着队列优先级
flags:为将来使用而保留的标识,此参数指定0

DISPATCH_QUEUE_PRIORITY_HIGH
DISPATCH_QUEUE_PRIORITY_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOW
DISPATCH_QUEUE_PRIORITY_BACKGROUND
*/

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(globalQueue, ^{
    NSLog(@"高优先级的并行队列globalQueue添加的任务1");
});

这里需要说明一点,使用dispatch_queue_create创建的队列(串行、并行),优先级和Global Dispatch Queue默认优先级相同。

4.4 dispatch_set_target_queue

这个方法可以改变队列的优先级,同时还可以将目标队列变成原队列的执行阶层。
dispatch_set_target_queue方法第一个参数是要变更优先级的队列1,第二个参数是目标优先级的队列2,最终队列1的执行优先级会变更成队列2的执行优先级,同时,队列2也将变成最终的执行阶层。
注意,队列1如果是系统指定的Main Dispatch QueueGlobal Dispatch Queue,那么结果不可预测。

//默认优先级串行队列
dispatch_queue_t serialDefaultQueue1 = dispatch_queue_create("com.example.myserialqueue", NULL);
//后台优先级并行队列
dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
//默认优先级 转换成了 后台优先级
dispatch_set_target_queue(serialDefaultQueue1, backgroundQueue);

创建4串行队列,使用dispatch_async将4个任务异步地分别加入到4个队列,这就意味着4个串行队列并行执行,所以执行顺序无法确定,如果这个时候指定一个串行队列作为执行阶层,那么结果就能确定下来。

dispatch_queue_t serialDefaultQueue1 = dispatch_queue_create("com.example.myserialqueue1", NULL);
dispatch_queue_t serialDefaultQueue2 = dispatch_queue_create("com.example.myserialqueue2", NULL);
dispatch_queue_t serialDefaultQueue3 = dispatch_queue_create("com.example.myserialqueue3", NULL);
dispatch_queue_t serialDefaultQueue4 = dispatch_queue_create("com.example.myserialqueue4", NULL);

dispatch_async(serialDefaultQueue1, ^{
    NSLog(@"111");
});
dispatch_async(serialDefaultQueue2, ^{
    NSLog(@"222");
});
dispatch_async(serialDefaultQueue3, ^{
    NSLog(@"333");
});
dispatch_async(serialDefaultQueue4, ^{
    NSLog(@"444");
});

/*
这个时候打印结果不确定,可能是:
111
222
333
444
也可能是:
111
333
222
444
*/

//指定一个串行队列作为执行阶层
dispatch_queue_t serialDefaultQueue = dispatch_queue_create("com.example.myserialqueue", NULL);

dispatch_set_target_queue(serialDefaultQueue1, serialDefaultQueue);
dispatch_set_target_queue(serialDefaultQueue2, serialDefaultQueue);
dispatch_set_target_queue(serialDefaultQueue3, serialDefaultQueue);
dispatch_set_target_queue(serialDefaultQueue4, serialDefaultQueue);

dispatch_async(serialDefaultQueue1, ^{
    NSLog(@"111");
});
dispatch_async(serialDefaultQueue2, ^{
    NSLog(@"222");
});
dispatch_async(serialDefaultQueue3, ^{
    NSLog(@"333");
});
dispatch_async(serialDefaultQueue4, ^{
    NSLog(@"444");
});
/*
打印结果固定为:
111
222
333
444
*/

这就防止了并行执行。

4.5 dispatch_after

这个方法作用是:在指定时间后,将指定任务添加到指定队列中。注意是把指定任务添加到队列中,并不是开始执行,如果指定队列中有阻塞的情况,那么任务执行的时间可能不准确,但是在大多数情况下,这个方法都是有效的。
实际开发中经常有需要延迟执行场景,这个时候就可以使用这个方法。

/*
参数:
when:传入DISPATCH_TIME_NOW以创建相对于当前的新时间值,DISPATCH_TIME_NOW其实是0ull
delta:在when基础上的时间跨度,注意是单位是纳秒,NSEC_PER_SEC代表1秒,还有毫秒宏NSEC_PER_MSEC
*/
/*
ull:C语言数值字面量,显示表示类型时使用的字符串(unsigned long long)
也可以如下这么写,int64_t是(long long)类型的别名
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC);
*/

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
/*
dispatch_after,第一个参数是dispatch_time_t类型的值,第二个参数是指定队列,第三个参数是任务block
*/
dispatch_after(time, dispatch_get_main_queue(), ^{
    NSLog(@"3s后将此任务加入至主队列");
});

延伸:dispatch_time_t类型的值,除了使用dispatch_time函数生成外,还可以通过dispatch_walltime函数生成,dispatch_time函数一般用于计算相对时间,而dispatch_walltime用于计算绝对时间。

/*
参数:
when:一个struct timespec类型的值的地址,如果传NULL,就代表自动获取当前时区的当前时间作为开始时间,相当于DISPATCH_TIME_NOW
delta:同上
*/
dispatch_time_t time = dispatch_walltime(NULL, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
    NSLog(@"3s后将此任务加入主队列");
});

4.6 Dispatch Group

这是一个“组”的概念,实际开发中可能会遇到这样一个场景:同时开展多个下载任务,等所有的下载成功后,通知用户下载完成。那么你就可以用到这个方法。这个方法的使用场景是当追加到并行队列中的多个任务全部执行完毕后,继续执行后续操作的时候。

/*
三部分:
1.dispatch_group_t,指定组
2.dispatch_group_async方法和dispatch_async方法一样,异步地将任务添加至指定队列中,不同的是多了一个dispatch_group_t类型的参数
3.dispatch_group_notify方法,可以检测到group中任务全部执行完成,将后续任务追加到指定队列中去。
*/

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, globalQueue, ^{
    NSLog(@"并行队列globalQueue中添加任务1");
});
dispatch_group_async(group, globalQueue, ^{
    NSLog(@"往并行队列globalQueue中添加任务2");
});
dispatch_group_async(group, globalQueue, ^{
    NSLog(@"往并行队列globalQueue中添加任务3");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"3个任务执行完毕,进行下一步操作");
});
/*
执行结果:
往并行队列globalQueue中添加任务1
往并行队列globalQueue中添加任务2
往并行队列globalQueue中添加任务3
3个任务执行完毕,进行下一步操作
*/

4.7 dispatch_barrier_async

从字面意思看,barrier意味着屏障,起到一个阻拦的作用,没错,这个方法会等待追加到并行队列上的一些任务全部结束之后,再将指定任务追加到该并行队列中并开始执行,结束后继续追加后续任务。
放入一个这样的场景便于更容易理解:球队上半场比赛、中场休息啦啦队表演、下半场比赛。dispatch_barrier_async就是隔离上半场和下半场比赛的啦啦队表演。

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.myconcurrentqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
    NSLog(@"科比拿球得分");
});
dispatch_async(concurrentQueue, ^{
    NSLog(@"老詹拿球得分");
});
dispatch_barrier_async(concurrentQueue, ^{
    NSLog(@"向我们走来的是啦啦队方阵,“你们都别打了,看啦啦队表演不香吗”");
});
dispatch_async(concurrentQueue, ^{
    NSLog(@"科比继续拿球得分");
});
dispatch_async(concurrentQueue, ^{
    NSLog(@"老詹继续拿球得分");
});
dispatch_async(concurrentQueue, ^{
    NSLog(@"科比投中最后一球扳平比分,把比赛拖入加时...");
    NSLog(@"R.I.P.");
});
/*
运行结果:
科比拿球得分
老詹拿球得分
向我们走来的是啦啦队方阵,“你们都别打了,看啦啦队表演不香吗”
科比继续拿球得分
老詹继续拿球得分
科比投中最后一球扳平比分,把比赛拖入加时...
R.I.P.
*/

使用Concurrent Dispatch Queuedispatch_barrier_async可以实现高效率的数据库访问和文件访问。

4.8 dispatch_apply

这个方法按指定的次数将指定的任务Block追加到指定的队列中,并等待全部执行完毕。这里面有一个循环的概念。它的作用和dispatch_syncDispatch Group有关联性,我们来看一下:

/*
第一个参数是重复次数,第二个参数是目标队列,第三个参数的Block是带有参数的Block,可以理解为索引。
*/
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.myconcurrentqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(5, concurrentQueue, ^(size_t index) {
    NSLog(@"添加任务%zu",index);
});
NSLog(@"任务执行结束");
/*
打印结果:
添加任务0
添加任务1
添加任务2
添加任务3
添加任务4
任务执行结束
*/
//任务执行的顺序不定,因为它们被添加进一个并行队列中,但是‘任务执行结束’一定在所有任务执行完毕后开始执行。

另外,dispatch_apply函数和dispatch_sync函数相同,都会等待任务执行结束,所以,可以在dispatch_async函数中调用dispatch_apply来模拟dispatch_sync的效果。

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.myconcurrentqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
    dispatch_apply(5, concurrentQueue, ^(size_t index) {
        NSLog(@"添加任务%zu",index);
    });
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"回到主队列(主线程)中刷新页面");
    });
});
/*
打印结果:
添加任务0
添加任务1
添加任务2
添加任务3
添加任务4
回到主队列(主线程)中刷新页面
*/

4.9 dispatch_suspend 和 dispatch_resume

这两个函数的作用是挂起指定队列恢复指定队列。简单来说,就是暂停队列上的任务执行和恢复队列上的任务执行。
用法很简单:

dispatch_suspend(queue); //挂起队列queue
dispatch_resume(queue); //恢复队列queue

说明一下,系统方法dispatch_get_global_queue获取的队列,不受这两个函数的影响。
通过一个示例来看一下:

//用dispatch_queue_create定义一个default优先级的并行队列
dispatch_queue_t queue = dispatch_queue_create("com.example.myserialqueue", DISPATCH_QUEUE_CONCURRENT);

//通过系统方法dispatch_get_global_queue来获取一个default优先级的并行队列
//dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
dispatch_async(queue, ^{
    for (NSInteger i = 0; i < 10; i++) {
        NSLog(@"0: == %@", [NSThread currentThread]);
        sleep(1); //每隔1s打印
    }
    });
sleep(4); //4s后开始挂起队列
NSLog(@"挂起队列");
dispatch_suspend(queue);
sleep(5); //5s后开始插入新任务
NSLog(@"开始插入新任务");
dispatch_async(queue, ^{
    for (NSInteger i = 0; i < 10; i++) {
        NSLog(@"1: == %@", [NSThread currentThread]);
        sleep(1); 
    }
});
NSLog(@"插入完成");
sleep(10);
NSLog(@"恢复队列"); //恢复队列后,插入的新任务开始执行
dispatch_resume(queue);

/*
打印结果:
2020-02-16 19:35:00.433102+0800 GCD[40594:1359769] 0: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:01.433533+0800 GCD[40594:1359769] 0: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:02.436353+0800 GCD[40594:1359769] 0: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:03.439585+0800 GCD[40594:1359769] 0: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:04.434346+0800 GCD[40594:1359623] 挂起队列
2020-02-16 19:35:04.445024+0800 GCD[40594:1359769] 0: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:05.450634+0800 GCD[40594:1359769] 0: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:06.453225+0800 GCD[40594:1359769] 0: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:07.453882+0800 GCD[40594:1359769] 0: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:08.455136+0800 GCD[40594:1359769] 0: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:09.434864+0800 GCD[40594:1359623] 开始插入任务
2020-02-16 19:35:09.435325+0800 GCD[40594:1359623] 插入完成
2020-02-16 19:35:09.456268+0800 GCD[40594:1359769] 0: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:19.436013+0800 GCD[40594:1359623] 恢复队列
2020-02-16 19:35:19.436471+0800 GCD[40594:1359769] 1: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:20.442080+0800 GCD[40594:1359769] 1: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:21.444556+0800 GCD[40594:1359769] 1: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:22.448543+0800 GCD[40594:1359769] 1: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:23.451605+0800 GCD[40594:1359769] 1: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:24.456389+0800 GCD[40594:1359769] 1: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:25.460112+0800 GCD[40594:1359769] 1: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:26.465718+0800 GCD[40594:1359769] 1: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:27.471063+0800 GCD[40594:1359769] 1: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
2020-02-16 19:35:28.472063+0800 GCD[40594:1359769] 1: == <NSThread: 0x600001974f00>{number = 6, name = (null)}
*/

/*
我们把上面的:  
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
注释放开,打印结果:
2020-02-17 12:58:10.483885+0800 GCD[45672:1606365] 0: == <NSThread: 0x600002efe840>{number = 3, name = (null)}
2020-02-17 12:58:11.477525+0800 GCD[45672:1606317] 挂起队列
2020-02-17 12:58:11.488415+0800 GCD[45672:1606365] 0: == <NSThread: 0x600002efe840>{number = 3, name = (null)}
2020-02-17 12:58:12.489070+0800 GCD[45672:1606365] 0: == <NSThread: 0x600002efe840>{number = 3, name = (null)}
2020-02-17 12:58:13.489453+0800 GCD[45672:1606365] 0: == <NSThread: 0x600002efe840>{number = 3, name = (null)}
2020-02-17 12:58:14.490094+0800 GCD[45672:1606365] 0: == <NSThread: 0x600002efe840>{number = 3, name = (null)}
2020-02-17 12:58:15.492254+0800 GCD[45672:1606365] 0: == <NSThread: 0x600002efe840>{number = 3, name = (null)}
2020-02-17 12:58:16.479144+0800 GCD[45672:1606317] 开始插入新任务
2020-02-17 12:58:16.479525+0800 GCD[45672:1606317] 插入完成
2020-02-17 12:58:16.479654+0800 GCD[45672:1606364] 1: == <NSThread: 0x600002eff100>{number = 5, name = (null)}
2020-02-17 12:58:16.492969+0800 GCD[45672:1606365] 0: == <NSThread: 0x600002efe840>{number = 3, name = (null)}
2020-02-17 12:58:17.483883+0800 GCD[45672:1606364] 1: == <NSThread: 0x600002eff100>{number = 5, name = (null)}
2020-02-17 12:58:18.486778+0800 GCD[45672:1606364] 1: == <NSThread: 0x600002eff100>{number = 5, name = (null)}
2020-02-17 12:58:19.489055+0800 GCD[45672:1606364] 1: == <NSThread: 0x600002eff100>{number = 5, name = (null)}
2020-02-17 12:58:20.492456+0800 GCD[45672:1606364] 1: == <NSThread: 0x600002eff100>{number = 5, name = (null)}
2020-02-17 12:58:21.494958+0800 GCD[45672:1606364] 1: == <NSThread: 0x600002eff100>{number = 5, name = (null)}
2020-02-17 12:58:22.499236+0800 GCD[45672:1606364] 1: == <NSThread: 0x600002eff100>{number = 5, name = (null)}
2020-02-17 12:58:23.502472+0800 GCD[45672:1606364] 1: == <NSThread: 0x600002eff100>{number = 5, name = (null)}
2020-02-17 12:58:24.505476+0800 GCD[45672:1606364] 1: == <NSThread: 0x600002eff100>{number = 5, name = (null)}
2020-02-17 12:58:25.506693+0800 GCD[45672:1606364] 1: == <NSThread: 0x600002eff100>{number = 5, name = (null)}
2020-02-17 12:58:26.480268+0800 GCD[45672:1606317] 恢复队列
*/
任务1是插入新任务后是立即执行的,说明队列并没有被挂起。

打印结果也说明,当队列被挂起时,正在执行中的任务无法停止,没开始的任务会在队列恢复后,继续执行。

4.10 Dispatch Semaphore

Dispatch Semaphore,信号量,类似十字路口协调交通用的手旗,竖起禁行,放下通行,或者也可以类比找车位,停车场有3个车位,3辆车可以停满,第4辆车必须等前面3辆车至少开走一辆。
Dispatch Semaphore函数运用传统计数,来控制对资源的并行访问,防止读写的时候发生数据竞争,粒度比dispatch_barrier_async更细。 它由三个部分构成:

  • dispatch_semaphore_create:这个方法使用初始值创建一个新的计数信号量,参数是一个不小于0的long型,例如:dispatch_semaphore_create(3),这里的字面量1,就表示上面所说的3个停车位
  • dispatch_semaphore_wait:信号量-1,相当于开进了一辆车,占用一个车位,当然如果没有车位,就会等待,参数有两个,第一个参数是信号量对象,第二个参数是超时时间,返回值是long型,如果成功,则返回0,这个时候可以安全地继续进行排他控制的处理;如果超时,则返回非0。
  • dispatch_semaphore_signal:信号量+1,相当于开走了一辆车,空出了一个车位,参数是信号量对象,例如dispatch_semaphore_signal(semaphore)

下面看一段示例代码:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//创建一个信号量,信号计数为1,保证每次操作数组都只访问单一线程,避免产生内存错误导致崩溃的情况
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100; i++) {
    dispatch_async(queue, ^{
        //等待信号量的计数>=1,在这之前一直等待
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        /*
            如果计数>=1,dispatch_semaphore_wait函数就把计数-1,来到这一步,这个时候计数=0,能执行到这一步的时候,访问数组的线程只有一个,因此数组可以安全地进行插入元素操作
        */
        NSLog(@"%i",i);
        [array addObject:[NSNumber numberWithInt:i]];
        /*
            到这一步,排他操作已经完成,所以要将信号量的计数通过dispatch_semaphore_signal函数+1,之后开始下一轮排他操作
        */
        dispatch_semaphore_signal(semaphore);
    });
}
/*
打印结果:
2020-02-17 23:02:16.715600+0800 GCD[60612:3749804] 0
2020-02-17 23:02:16.715724+0800 GCD[60612:3749807] 1
2020-02-17 23:02:16.715810+0800 GCD[60612:3749805] 2
2020-02-17 23:02:16.715929+0800 GCD[60612:3749803] 3
2020-02-17 23:02:16.716040+0800 GCD[60612:3749804] 4
2020-02-17 23:02:16.716141+0800 GCD[60612:3749807] 5
2020-02-17 23:02:16.716214+0800 GCD[60612:3749806] 6
2020-02-17 23:02:16.716286+0800 GCD[60612:3749812] 7
2020-02-17 23:02:16.716528+0800 GCD[60612:3749805] 8
2020-02-17 23:02:16.716778+0800 GCD[60612:3749803] 9
2020-02-17 23:02:16.717029+0800 GCD[60612:3749804] 10
2020-02-17 23:02:16.717295+0800 GCD[60612:3749807] 11
2020-02-17 23:02:16.717609+0800 GCD[60612:3749806] 12
2020-02-17 23:02:16.717871+0800 GCD[60612:3749813] 13
...
从打印结果来看,i的递增是规律的,数组的插入元素操作也是正常的
*/

从打印结果来看,i的递增是规律的,数组的插入元素操作也是正常的
但是如果不用这个排他控制操作,我们再来看一下:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//创建一个信号量,信号计数为1,保证每次操作数组都只访问单一线程,避免产生内存错误导致崩溃的情况
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100; i++) {
    dispatch_async(queue, ^{
        NSLog(@"%i",i);
        [array addObject:[NSNumber numberWithInt:i]];
    });
}
打印结果:
...
2020-02-17 23:05:05.743966+0800 GCD[60662:3751751] 52
2020-02-17 23:05:05.744083+0800 GCD[60662:3751756] 53
2020-02-17 23:05:05.744241+0800 GCD[60662:3751753] 54
2020-02-17 23:05:05.744403+0800 GCD[60662:3751752] 55
GCD(60662,0x700003214000) malloc: *** error for object 0x7fed6ad02270: pointer being freed was not allocated
GCD(60662,0x700003214000) malloc: *** set a breakpoint in malloc_error_break to debug
...

这个时候就发生了内存访问错误

4.11 dispatch_once

dispatch_once函数是保证应用程序执行中只执行一次指定处理的API,当写单例的时候会经常见到。 dispatch_once_t是long类型参数,dispatch_once函数的第一个参数predicate,是一个指针,指向一个dispatch_once_t类型参数

针对于predicate的官方文档:
A pointer to a dispatch_once_t structure that is used to test whether the block has completed or not.
指向一个dispatch_once_t类型参数的指针,用于测试block中代码是否已运行。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    NSLog(@"这个任务只会执行一次");
});

这里,dispatch_once函数其实可以执行多次,涉及到的底层方法较多,暂时不展开讲。

4.12 Dispatch I/O

当读写较大文件时,如果可以将大文件分成若干部分分别读写,那么效率上会高出不少,Dispatch I/O就可以实现这个功能,它可以将一个文件,分成若干指定大小,并行读写。 下面是一段线性读取本地文件的代码:

//本地文件地址
NSString *path = [[NSBundle mainBundle] pathForResource:@"11111.gif" ofType:@""];
//创建一个队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
参数
type:通道的类型,unsigned long,分为DISPATCH_IO_STREAM和DISPATCH_IO_RANDOM
    DISPATCH_IO_STREAM:代表着该通道字节流是线性的,按顺序读写文件
    DISPATCH_IO_RANDOM:代表着该通道随机性的访问文件,不是按照顺序读写文件
path:文件路径的C字符串
oflag:打开路径时传递给open函数的标志
mode:在指定路径创建文件时传递给open函数的模式。如果不创建文件,指定为0。
queue:任务队列
cleanup_handler:指定的发生错误时用来执行处理的Block
*/
//通过目标文件的路径,创建一个通道
dispatch_io_t channel = dispatch_io_create_with_path(DISPATCH_IO_STREAM, [path UTF8String], 0, 0, queue, ^(int error) {
    if (error == 0) {
        NSLog(@"创建channel正常");
    }else {
        NSLog(@"创建channel发生未知错误");
    }
});
size_t lowWater = 1024;
//size_t highWater = 2048;
//设定一次读写的最小值
dispatch_io_set_low_water(channel, lowWater);
//设定一次读写的最大值
//dispatch_io_set_high_water(channel, highWater);
NSMutableData *totalData = [[NSMutableData alloc] init];
/*
读取操作
参数
channel:通道
offset:对于DISPATCH_IO_RANDOM通道,此参数指定要从中读取的通道的偏移量。偏移量是相对于创建通道时通道文件描述符的初始文件指针指定的。对于DISPATCH_IO_STREAM通道,忽略
length:从通道读取的字节数。指定SIZE_MAX,直到结束。
queue:队列
io_handler:一个处理数据读取相关的block,有三个参数
    done:标记是否已读取完成,布尔类型
    data:dispatch_data_t类型的对象,代表着读取到的数据。
    error:如果有错误,则为错误类型的值;没有错误,error值为0。
*/
dispatch_io_read(channel, 0, SIZE_MAX, queue, ^(bool done, dispatch_data_t  _Nullable data, int error) {
    if (error == 0) {
        size_t size = dispatch_data_get_size(data);
        if (size > 0 ) {
            [totalData appendData:(NSData*)data]; //拼接数据
        }
    }
    if (done) {
        NSLog(@"文件已经读取完毕:%@",totalData);
    }
});

  1. 在串行队列Serial Dispatch Queue中,需要等待当前执行的任务完成才能开始下一个任务,因此只使用一个线程; 在并行队列Concurrent Dispatch Queue中,不用等待当前执行中的任务结束就可以开始下一个任务,所以会开启多个线程同时执行多个任务,具体开启线程的数量、任务处理的调度以及处理结束后相关线程的结束,是由XNU内核完成的↩︎

  2. 串行队列:等待当先任务执行处理的队列Serial Dispatch Queue
    并行队列:不等待当前任务执行处理的队列Concurrent Dispatch Queue ↩︎