多线程系列篇章计划内容:
iOS多线程编程(一) 多线程基础
iOS多线程编程(二) Pthread
iOS多线程编程(三) NSThread
iOS多线程编程(四) GCD
iOS多线程编程(五) GCD的底层原理
iOS多线程编程(六) NSOperation
iOS多线程编程(七) 同步机制与锁
iOS多线程编程(八) RunLoop
前言
本文主要介绍GCD相关概念以及使用,对于GCD的核心概念、函数和队列的搭配使用、函数和队列的复杂组合示例以及GCD中的线程同步机制做了详细的分析。未做底层源码分析,若想了解GCD底层原理分析,可移步 iOS多线程编程(五) GCD的底层原理。
附:iOS下的多线程方案:1. GCD简介
GCD 全称 Grand Central Dispatch,基于C语言实现的多线程机制,是Apple提供的一个多核编程的解决方案。它允许将一个程序切分为多个单一任务,然后提交到工作队列中并发或串行地执行。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。
他的优势体现在:
- GCD 可用于多核的并行运算,会自动合理地利用 CPU内核(比如双核、四核)。
- GCD 使用简单,开发者要做的只是定义执行的 任务,追加到适当的 队列 中,并且指定执行任务的 函数。配合Block,使用起来也更加方便灵活。
- GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。
2. GCD的核心概念
在开发中,我们通常是这样使用GCD的。
dispatch_async(dispatch_queue_create("com.xxx.testqueue", DISPATCH_QUEUE_CONCURRENT), ^{
NSLog(@"gcd test");
});
GCD使用的本质,就是 “定义要执行的『任务』,将任务添加到『队列』中,并且指定执行任务的『函数』”。怎么理解这句话呢?不妨将上面的示例代码拆分开来:
// 1.定义要执行的任务(打印 “gcd test”)
dispatch_block_t task = ^{
NSLog(@"gcd test");
};
// 2.指定任务的目标队列(并发队列)
dispatch_queue_t queue = dispatch_queue_create("com.xxx.testqueue", DISPATCH_QUEUE_CONCURRENT);
// 3.使用异步函数,将任务提交到目标队列
dispatch_async(queue, task);
这样就比较清晰了,对于GCD的使用有三个要素:『任务』、『队列』和 『函数』。
2.1 任务
任务就是要执行的操作,在GCD中也就是在Block内的代码段,任务的Block没有参数也没有返回值。
2.2 函数
函数决定了任务的执行方式。『同步执行』 还是 『异步执行』。两者的主要区别是:是否需要等待当前任务的返回结果,以及是否具备开启新线程的能力。
2.2.1 同步函数(sync)
- 同步函数执行的任务,调用一旦开始,调用者必须等待任务执行完毕,才能继续执行后续行为。
- 不具备开启新线程的能力(任务只能在当前线程中执行任务)。
2.2.2 异步函数(async)
异步函数执行的任务,调用者无需等待任务执行完毕,就可以继续执行后续行为。
具备开启新线程的能力(可以在新的线程中执行任务)。
需要注意的是:异步执行虽然具有开启新线程的能力,但并不一定要开启新线程,还与任务所属队列有关(如异步执行主队列中的任务就不会开启新线程)。
通常,我们也将同步执行的任务记为同步任务,异步执行的任务记为异步任务。
2.3 队列
队列是一种特殊的线性表,它只允许在表的前端进行删除操作,在表的后端进行插入操作。所以只有最早进入队列的元素才能最先从队列中删除。因此队列的基本特性就是 FIFO(先进先出) 原则。
在GCD中,我们需要将任务添加到队列中,新任务总是被插入到队列的末尾,而调度任务的时候总是从队列的头部开始执行。每调度一个任务,则从队列中移除该任务。
在GCD中有两种队列:串行队列 和 并发队列。两者都遵循FIFO原则,两者的主要区别是:执行顺序不同,使用的线程个数不同。
2.3.1 串行队列
串行队列每次只有一个任务被调度,任务一个接一个地执行,且任务都在同一个线程执行。只有任务1被调度完毕才能调度任务2,以此类推。
2.3.2 并发队列
并发队列可以让多个任务并发(”同时“)执行。这取决于有多少可以利用的线程,假设在两条线程可用的情况下,那么任务1和任务2可分别在不同线程中并发执行。
需要注意的是:并发队列 的并发功能只有在 异步函数 下才有效。
如果从执行时间
上对比两者的区别:那么同一时刻,串行队列
只有一个任务在执行,而并发队列
在同一时刻可能有多个任务在执行,并且,哪个任务先执行完毕也是不确定的(这受任务复杂度
以及CPU调度
的影响)。
如上图,在并发队列中,红线位置表示,同一时间任务2、3、4都在执行,并且任务4先于任务3调度(CPU的调度
),但是任务3的复杂度较低,所以任务3先于任务4执行完毕(任务复杂度
)。而对于串行队列,同一时间只能有一个任务在执行,并且任务严格按照队列中的顺序执行。
更为严谨的说法,就要考虑
并发
与并行
的区别了,并行
是多核下真正的同时执行,而并发
则是由CPU时间片的轮转机制,让我们看起来好像是同时执行一样。从宏观上,我们可以将两者看成是一回事,因为CPU的时间粒度实在是太小了。
2.3.3 主队列
主队列是一种特殊的 串行队列,在libdispatch_init
初始化时就创建了主队列,并且完成了与主线程的绑定。这些都是在程序main()
函数之前就已完成的。
也就是说程序完成启动之时就已经有了主队列,并且所有放在主队列
中的任务都是在主线程
中执行的。不管是同步还是异步都不会开辟新线程,任务只会在主线程执行。这也是通常在主线程刷新UI时会将任务放到主队列的原因。
可通过dispatch_get_main_queue()
获取主队列。
2.3.4 全局并发队列
全局并发队列 本质上是一个并发队列,由系统提供,方便编程,不用创建就可使用。
可通过dispatch_get_global_queue(long indentifier.unsigned long flags)
获取全局并发队列。
该函数提供了两个参数,第一个参数表示队列优先级,通常写0,也就是默认优先级。可以通过服务质量类值来获取不同优先级的全局并发队列。
* - QOS_CLASS_USER_INITIATED
* - QOS_CLASS_DEFAULT
* - QOS_CLASS_UTILITY
* - QOS_CLASS_BACKGROUND
也可以通过队列的优先级来识别,他们的映射关系如下:
* - DISPATCH_QUEUE_PRIORITY_HIGH: QOS_CLASS_USER_INITIATED
* - DISPATCH_QUEUE_PRIORITY_DEFAULT: QOS_CLASS_DEFAULT
* - DISPATCH_QUEUE_PRIORITY_LOW: QOS_CLASS_UTILITY
* - DISPATCH_QUEUE_PRIORITY_BACKGROUND: QOS_CLASS_BACKGROUND
第二个参数为预留参数。建议写0,因为Apple说,传递除零之外的任何值都可能导致空返回值。
关于队列、任务与线程,有篇文章的类比的描述的比较好理解,复制供大家理解:
假设现在有 5 个人要穿过一道门禁,这道门禁总共有 10 个入口,管理员可以决定同一时间打开几个入口,可以决定同一时间让一个人单独通过还是多个人一起通过。不过默认情况下,管理员只开启一个入口,且一个通道一次只能通过一个人。
这个故事里,人好比是 任务,管理员好比是 系统,入口则代表 线程。
- 5 个人表示有 5 个任务,10 个入口代表 10 条线程。
- 串行队列 好比是 5 个人排成一支长队。
- 并发队列 好比是 5 个人排成多支队伍,比如 2 队,或者 3 队。
- 同步任务 好比是管理员只开启了一个入口(当前线程)。
- 异步任务 好比是管理员同时开启了多个入口(当前线程 + 新开的线程)。
『异步执行 + 并发队列』 可以理解为:现在管理员开启了多个入口(比如 3 个入口),5 个人排成了多支队伍(比如 3 支队伍),这样这 5 个人就可以 3 个人同时一起穿过门禁了。
『同步执行 + 并发队列』 可以理解为:现在管理员只开启了 1 个入口,5 个人排成了多支队伍。虽然这 5 个人排成了多支队伍,但是只开了 1 个入口啊,这 5 个人虽然都想快点过去,但是 1 个入口一次只能过 1 个人,所以大家就只好一个接一个走过去了,表现的结果就是:顺次通过入口。换成 GCD 里的语言就是说:
- 『异步执行 + 并发队列』就是:系统开启了多个线程(主线程+其他子线程),任务可以多个同时运行。
- 『同步执行 + 并发队列』就是:系统只默认开启了一个主线程,没有开启子线程,虽然任务处于并发队列中,但也只能一个接一个执行了。
3. 队列与函数的搭配
GCD中有两种队列(串行/并发),两种任务执行方式(同步/异步),那么自然就有这样的四种组合使用方式:
1.同步执行+串行队列
2.同步执行+并发队列
3.异步执行+串行队列
4.异步执行+并发队列
全局并发队列和我们自定义的普通并发队列关于同步/异步
执行结果是相同的。而对于主队列,它是一种特殊的串行队列,只要在主队列中的任务一定会在主线程
中执行。我们将主队列也考虑其中,这样就有六种组合使用方式:
5.同步执行+主队列
6.异步执行+主队列
接下来我们探讨这几种不同组合的区别有哪些?
3.1 同步+串行
// 同步+串行 任务
- (void)sync_serial{
// 打印当前线程
NSLog(@"currentThread---%@",[NSThread currentThread]);
NSLog(@"begin");
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("com.xxx.queue", DISPATCH_QUEUE_SERIAL);
// 任务 1
dispatch_sync(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务1---%@",[NSThread currentThread]); // 打印当前线程
});
// 任务 2
dispatch_sync(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务2---%@",[NSThread currentThread]); // 打印当前线程
});
// 任务 3
dispatch_sync(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"end");
}
打印结果:
currentThread---{number = 1, name = main}
begin
任务 1---{number = 1, name = main}
任务 2---{number = 1, name = main}
任务 3---{number = 1, name = main}
end
可见,同步+串行
任务中
- 所有的任务都是在当前线程(此例中为主线程)中执行的,
未开启新线程
。(同步执行
不具备开启新线程的能力) - 任务完全按照自上至下按
顺序执行
(同步执行
需等待当前任务执行完毕才能继续向下执行)
3.2 同步+并发
// 同步+并发 任务
- (void)sync_concurrent{
// 打印当前线程
NSLog(@"currentThread---%@",[NSThread currentThread]);
NSLog(@"begin");
// 并发队列
dispatch_queue_t queue = dispatch_queue_create("com.xxx.queue", DISPATCH_QUEUE_CONCURRENT);
// 任务 1
dispatch_sync(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 1---%@",[NSThread currentThread]); // 打印当前线程
});
// 任务 2
dispatch_sync(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 2---%@",[NSThread currentThread]); // 打印当前线程
});
// 任务 3
dispatch_sync(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"end");
}
打印结果:
currentThread---{number = 1, name = main}
begin
任务 1---{number = 1, name = main}
任务 2---{number = 1, name = main}
任务 3---{number = 1, name = main}
end
同步+并发
从结果来看与上面的同步+串行是一样的。
- 所有任务都是在当前线程(此例中为主线程)执行,未开启新线程(
同步执行
不具备开启新线程的能力)。 - 任务自上至下顺序执行。(
同步执行
需等待当前任务执行完毕才能继续向下执行)。
前面在说并发队列的时候,我们说 并发队列 可以让多个任务并发执行,那这里为什么任务还是按照顺序执行的呢?
任务的执行者是线程而非队列,虽然并发队列支持多任务同时执行,但同步并不具备开启线程的能力,只能利用当前线程,当前线程只有一个(主线程),所以任务还是依次在主线程中执行。
3.3 异步+串行
// 异步+串行 任务
- (void)async_serial{
// 打印当前线程
NSLog(@"currentThread---%@",[NSThread currentThread]);
NSLog(@"begin");
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("com.xxx.queue", DISPATCH_QUEUE_SERIAL);
// 任务 1
dispatch_async(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 1---%@",[NSThread currentThread]); // 打印当前线程
});
// 任务 2
dispatch_async(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 2---%@",[NSThread currentThread]); // 打印当前线程
});
// 任务 3
dispatch_async(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"end");
}
打印结果:
currentThread---{number = 1, name = main}
begin
end
任务 1---{number = 6, name = (null)}
任务 2---{number = 6, name = (null)}
任务 3---{number = 6, name = (null)}
可见,异步+串行
任务中
- 开启了新线程(
异步执行
具有开启线程的能力)但是不管任务有多少个,只开启一条新线程(串行队列
的任务都在同一条线程执行)。 - 所有的任务都是在
begin
和end
之后执行的(异步执行
不需等待任务完毕,就可继续向下执行)。 - 任务是按队列中的顺序执行的(
串行队列
每次只有一个任务被执行,任务一个接一个执行)。
3.4 异步+并发
// 异步+并发 任务
- (void)async_concurrent{
// 打印当前线程
NSLog(@"currentThread---%@",[NSThread currentThread]);
NSLog(@"begin");
// 并发队列
dispatch_queue_t queue = dispatch_queue_create("com.xxx.queue", DISPATCH_QUEUE_CONCURRENT);
// 任务 1
dispatch_async(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 1---%@",[NSThread currentThread]); // 打印当前线程
});
// 任务 2
dispatch_async(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 2---%@",[NSThread currentThread]); // 打印当前线程
});
// 任务 3
dispatch_async(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"end");
}
打印结果:
currentThread---{number = 1, name = main}
begin
end
任务 1---{number = 3, name = (null)}
任务 3---{number = 6, name = (null)}
任务 2---{number = 7, name = (null)}
可见,异步+并发
任务中
- 本例启动了三个线程,任务是无序的,交替执行(
异步执行
具备开启新线程的能力,并发队列可利用多个线程,同时执行多个任务)。 - 任务是在
begin
和end
之后开始执行的(异步执行
不需等待任务完毕,就可继续向下执行)。
3.5 同步+主队列
// 同步+主队列 任务
- (void)sync_main{
// 打印当前线程
NSLog(@"currentThread---%@",[NSThread currentThread]);
NSLog(@"begin");
// 主队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 任务 1
dispatch_sync(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 1---%@",[NSThread currentThread]); // 打印当前线程
});
// 任务 2
dispatch_sync(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 2---%@",[NSThread currentThread]); // 打印当前线程
});
// 任务 3
dispatch_sync(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"end");
}
打印结果:
currentThread---{number = 1, name = main}
begin
(lldb)
在同步+主队列
任务中,当运行至begin之后,程序崩溃退出了。这是因为:
当前线程为主线程,在主线程中执行 sync_main
,相当于将sync_main
加入主队列中,当追加任务1的时候,再将任务1加入主队列,由于主队列是串行队列,任务1 就需等待sync_main
执行完毕,而又因是同步执行,sync_main
需等待任务1执行完毕 ,这样的互相等待,就导致了死锁的发生。
所以,在主线程,执行“同步+主队列”任务时,
- 会导致死锁的发生。
事实上,只要是在当前串行队列中的同步任务都会导致死锁(见4.GCD 的函数与队列复杂组合示例 中的示例3、4)
但如果将“同步+主队列”任务放到其他线程(非主线程),那么并不会发生死锁,
- 所有的任务都将在主线程(而非当前线程)执行,且任务按序执行。
3.6 异步+主队列
// 异步+主队列 任务
- (void)async_main{
// 打印当前线程
NSLog(@"currentThread---%@",[NSThread currentThread]);
NSLog(@"begin");
// 主队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 任务 1
dispatch_async(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 1---%@",[NSThread currentThread]); // 打印当前线程
});
// 任务 2
dispatch_async(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 2---%@",[NSThread currentThread]); // 打印当前线程
});
// 任务 3
dispatch_async(queue, ^{
sleep(1); // 模拟耗时操作
NSLog(@"任务 3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"end");
}
打印结果:
currentThread---{number = 1, name = main}
begin
end
任务 1---{number = 1, name = main}
任务 2---{number = 1, name = main}
任务 3---{number = 1, name = main}
在异步+主队列
任务中。
- 所有的任务都是在主线程中执行的(虽然
异步执行
具备开启线程的能力,但因为是主队列,所以所有的任务都在主线程中)。 - 任务均在begin和end之后执行(
异步执行
不需等待任务完毕,就可继续向下执行)。 - 任务是按顺序执行(主队列是串行队列,每次只执行一个任务,任务一个接一个执行)。
小结
首先需要明确的是,任务是在『线程』上执行的。线程是由系统创建,不管是队列还是函数都没有创建线程的能力,只能启用。
『队列』只是存放任务的。不管串行队列还是并发队列,任务都是先进先出
的,这是队列的基本特性。只不过,串行队列中的任务一次只出一个,须前一个任务调度完毕后才能出下一个任务;而并发队列中的任务根据当前可用线程数出队列,如果当前可用线程数为3,就出3个任务,如果可用线程数为1,那么也就如同串行队列一样只出一个任务。
『函数』决定任务的执行方式。
只要是同步执行,当前线程就需等待任务执行完毕后才能继续后续行为;
只要是异步执行,当前线程调用了任务之后,就可以继续后续行为。
无论『队列』与『函数』如何搭配,都不会影响队列和函数的基本特性。
对于同步函数,因为要等待任务的返回结果,所以也就没有必要启动线程。
对于异步函数,因为当前线程此刻并不关心任务的结果,需要立即往后执行,所以任务自然也就不能放在当前线程,不然还是阻塞了当前线程,这与异步本就是相悖的。
所以:
- ① 只要是
同步执行
的任务,就不会开启新的线程,要么在当前线程执行,要么在主线程执行(主队列)。 - ② 只要是
异步执行
的任务,任务就不可能在调度任务的线程执行(主队列除外),要么开启线程,要么在主线程执行(主队列)。 - ③
主队列
很特殊,只要追加到主队列中的任务就一定要主线程执行,不管主线程有多忙,它会一直等待主线程空闲下来再执行。并且在主线程同步执行主队列任务时会发生死锁(事实上,在当前串行队列中,同步追加任务都是死锁,参看4.GCD 的函数与队列复杂组合示例 中的示例3、4)。 - ④ 只有
异步并发
的任务,才无序执行。
4. GCD 的函数与队列的组合示例
理解了 函数 和 队列 的特性之后,我们来看一下他们组合使用的情况。
示例1:异步并发任务的执行,下面代码输出的结果是什么?
- (void)textDemo1{
dispatch_queue_t queue = dispatch_queue_create("com.xxx.queue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
程序的代码是自上而下依次执行的,当前线程为主线程,首先创建了一个并发队列,接着主线程输出“1”;追加异步任务1
到并发队列,因为异步
的特性,所以主线程会接着向下执行,输出“5”;
并发队列中的异步任务
会开启子线程,所以接下来在子线程1中输出“2”;继而再次追加异步任务2
到并发队列,同样由于异步
的特性,当前子线程1不做等待直接向下执行,输出“4”,最后执行异步任务2
,开启了另一个子线程2,输出“3”。
所以结果为 1 5 2 4 3 。
需要注意的是:并不是只要是这种嵌套形式就一定是这样的顺序执行的,前提是各任务的耗时是相似的。如果在此例中
NSLog(@"5")
之前添加代码sleep(1)
让主线程休眠1秒钟模拟这一部分的任务是更复杂的,那么结果将会是 1 2 4 3 5。之前我们提到,任务的执行完毕时间取决于任务复杂度
和CPU的调度
。
示例2:同步任务在并发队列中的执行,下面代码输出的结果是什么?
- (void)textDemo2{
dispatch_queue_t queue = dispatch_queue_create("com.xxx.queue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
创建并发队列,首先输出“1”,继而追加异步任务
到并发队列,由于异步的特性,所以主线程继续执行,输出“5”,然后在异步任务
开启的子线程1中,输出“2”,继而追加同步任务
到并发队列,因为是同步任务
,所以当前线程1需等待同步任务的执行完毕,也就是需等待输出“3”执行完毕之后,才能继续输出“4”,所以答案为:1 5 2 3 4。
示例3:当前串行队列中的同步任务执行,下面代码输出的结果是什么?
- (void)textDemo3{
dispatch_queue_t queue = dispatch_queue_create("com.xxx.queue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
此例中创建的队列为串行队列,首先输出“1”,追加异步任务1
到串行队列中,由于是异步任务
,所以,主线程直接向下执行,输出“5”,
然后调度串行队列中的异步任务1
,在异步任务
块内,代码依然是顺序执行的,我们将任务细分来看,也就是串行队列中任务如下图布局,
根据FIFO原则,先调度NSLog(@"2")输出”2“,继而追加同步任务
到串行队列,将NSLog(@"3")任务入队,此时串行队列中任务如下图布局
因为最后追加的是同步任务
,所以,sync要等待NSLog(@"3")执行完毕才能继续向下执行,而又由于是串行队列,需保证任务按顺序执行,NSLog(@"3")需等待NSLog(@"4"),NSLog(@"4")需等待sync。
这样互相等待,就导致了死锁的发生,所以最终结果为 1 5 2 死锁。
在开启的子线程1中输出“2”,接下来追加同步任务1
到串行队列,此时串行队列中有两个任务(还未执行完毕的异步任务1和追加的同步任务1)。因为是NSLog(@"3")任务是同步执行的,所以队列内的异步任务
需要等待NSLog(@"3")执行完毕才能执行完,又因为是串行队列,所以NSLog(@"3")需要等待异步任务执行完毕。这样互相等待,就导致了死锁的发生。
示例4:那如果将NSLog(@"4");任务去掉呢?
- (void)textDemo4{
dispatch_queue_t queue = dispatch_queue_create("com.xxx.queue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
});
NSLog(@"5");
}
前面的分析过程同示例3是一样的:创建串行队列,主线程输出”1“,追加异步任务1
到串行队列,主线程输出”5“。
然后调度串行队列中的异步任务
,输出”2“,继续追加同步任务1
到串行队列,此时队列中有两个任务(未执行完毕的异步任务1
和刚追加的同步任务1
)
虽然异步任务
中没有NSLog(@"4")任务,但是整个异步任务
依然需要等待 同步任务
执行完毕,而由于串行队列,同步任务需要等待异步任务执行完毕,这样的互相等待还是会导致死锁。
示例5:下面看一个选择题示例
- (void) textDemo5{//
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
// 1 2 3
// 0 (7 8 9)
dispatch_async(queue, ^{
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"0");
dispatch_async(queue, ^{
NSLog(@"7");
});
dispatch_async(queue, ^{
NSLog(@"8");
});
dispatch_async(queue, ^{
NSLog(@"9");
});
// A: 1230789
// B: 1237890
// C: 3120798
// D: 2137890
}
示例中为并发队列,异步并发的情况,在同等水平的任务下是没有固定顺序的,输出”3“是同步任务,也就是说从输出”0“开始必须等待输出”3“执行完毕,所以”3“一定在”0“之前;
输出”0“之后的任务都是异步并发的,所以”7“、”8“、”9“一定在”0“之后。
满足条件的为A和C。
5.GCD线程间的通讯
在开发中,我们通常将耗时的任务放置到子线程,主线程继续处理其他任务,待子线程任务完成后,再通知主线程进行刷新UI等操作,那么如何在子线程中通知主线程执行任务?
其实很简单,就是借助主队列,因主队列中的任务一定是在主线程中执行的。
/**
* 线程间通信
*/
- (void)communication {
NSLog(@"begin");
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// 子线程执行
// 1. 获取图片 imageUrl
NSURL *imageUrl = [NSURL URLWithString:@"https://xxxxx.jpg"];
// 2. 从 imageUrl 中读取数据(下载图片) -- 耗时操作
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
// 3. 通过二进制 data 创建 image
UIImage *image = [UIImage imageWithData:imageData];
NSLog(@"1---%@",[NSThread currentThread]);
// 回到主线程
dispatch_async(mainQueue, ^{
self.imageView.image = image;
NSLog(@"2---%@",[NSThread currentThread]);
});
});
NSLog(@"end");
}
上述代码在子线程中请求图片数据,数据获取完毕后,回到主线程更新图片。打印的结果如下,是符合我们预期的。
begin
end
1---{number = 3, name = (null)}
2---{number = 1, name = main}
拓展:为什么一定要在主线程更新UI呢?
首先,UIKit不是线程安全的,当多个线程同时操作UI时,抢夺资源,有可能导致崩溃,UI异常等问题。假如在两个线程中设置了同一张背景图片,很有可能就会由于背景图片被释放两次,使得程序崩溃。或者某一个线程中遍历找寻某个subView,然而在另一个线程中删除了该subView,那么就会造成错乱。
那么为什么不将UIKit设计成线程安全的呢?为了性能
和效率
。
多线程访问必定会涉及线程同步
的开销问题,UIKit是一个庞大的框架,UI操作涉及到渲染访问各种View对象的属性,如果为了确保UI操作的线程安全,一来会带来巨大的成本(视图层级深,属性多),二来会耗费大量的资源拖慢运行速度(加锁)。这未必会带来更高的效率。
所以,UI的操作最好在单一线程里执行,那放到哪个线程呢?
在Cocoa Touch框架中,UIApplication初始化工作是在主线程
进行的,而界面上所有的视图都是在UIApplication实例的叶子节点上(内存管理角度),所以所有的用户交互事件都是在主线程上进行传递,在主线程响应。
在主线程
操作UI,能够帮助我们避免一些不必要的麻烦和缺陷,也就成了一个约定俗成的开发规则。
那么,子线程到底能不能更新UI呢?
有时也可以,但是会有问题。在子线程能更新的UI是一个假象,其实是子线程代码执行完毕了,又自动进入到了主线程,执行了子线程中的UI更新的函数栈,这中间的时间非常的短,就让大家误以为分线程可以更新UI。如果子线程一直在运行,则子线程中的UI更新的函数栈,主线程就无法获知,那就无法更新直到子线程结束。
6. GCD常用函数
在使用GCD时,我们通常使用”异步+并发“的形式,在子线程执行相关的任务,这可以让主线程继续执行后续操作,也可以充分利用CPU内核更快地执行多个任务。但是异步并发
的任务是无序执行的,并且哪一个任务先执行完毕也是不确定的。如果队列中某一个(组)任务的执行是依赖于另一个(组)任务的数据,该如何处理呢?
GCD提供的栅栏函数、调度组、信号量可以解决这一问题。
6.1栅栏函数:dispatch_barrier_async/sync
栅栏函数相当于在队列中的特定位置设立了一个”栅栏“,并且这个栅栏中也可以执行任务
只有当栅栏前面的任务全部调度完毕后才可以继续调度栅栏任务以及栅栏后面的任务。
/**
* 栅栏函数 dispatch_barrier_async
*/
- (void)barrier {
dispatch_queue_t queue = dispatch_queue_create("com.xxx.queue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"begin");
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1 ---%@",[NSThread currentThread]);// 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2 ---%@",[NSThread currentThread]);// 打印当前线程
});
dispatch_barrier_async(queue, ^{
// 追加任务 barrier
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3 ---%@",[NSThread currentThread]);// 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 4
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4 ---%@",[NSThread currentThread]);// 打印当前线程
});
NSLog(@"end");
}
打印结果:
begin
end
2 ---{number = 5, name = (null)}
1 ---{number = 4, name = (null)}
barrier---{number = 4, name = (null)}
3 ---{number = 4, name = (null)}
4 ---{number = 5, name = (null)}
如果将上例中的dispatch_barrier_async
函数换成dispatch_barrier_sync
,那么打印结果如下:
begin
2 ---{number = 6, name = (null)}
1 ---{number = 5, name = (null)}
barrier---{number = 1, name = main}
end
3 ---{number = 5, name = (null)}
4 ---{number = 8, name = (null)}
需要注意:在使用栅栏函数的时候,使用
自定义并发队列
才有意义。如果是全局并发队列,这个栅栏函数不起作用;如果用的是串行队列,因其本身就是按序执行。所以没有意义。
6.2 调度组:dispatch_group
在追加到队列中的多个任务全部执行完毕后,再执行接下来的处理操作,这种需求经常会出现在我们的程序中。
如果只使用一个串行队列时,只要将想要执行的任务全部追加到队列中,并在最后执行结束的操作即可。
如果是使用并发队列呢?或者使用多个不同队列,想要实现这种需求就变得相当复杂。
串行队列虽然可以实现这个需求,但耗时任务依然需要一个接一个执行,执行效率不高,更多的情况是,我们希望任务在异步的子线程去执行,而不影响用户交互与主线程事务。所以我们需要使用并发队列来完成此类需求,但是异步并发
的任务是在多个子线程中被调度的,且哪一个先执行完毕并不是确定的,这就给此类需求带来了极大的难度,对于不同队列中的任务的监控也是如此。所以GCD提供了调度组dispatch_group
。
dispatch_group中常用的函数如下:
dispatch_group_notify
dispatch_group_enter/dispatch_group_enter
dispatch_group_wait
调度组保存了与其相关联的待执行的任务的数量,当一个新的任务被关联的时候增加它的计数(+1),当一个任务执行完毕的时候减少它的计数(-1)。当所有与调度组相关联的任务全部完成的时候,调度组会通过dispatch_group_wait
或dispatch_group_notify
来通知整个组的任务完成。
- dispatch_group_notify:监听组内任务的执行完毕
在使用时,我们通过dispatch_group_async
函数将任务追加到指定队列中并且关联到调度组,这些队列可以不相关,也就是可以将任务提交到不同的队列中,只要关联的是同一个调度组。调度组会监听组内任务的执行情况,当所有与调度组相关联的任务全部完成的时候,就会回调dispatch_group_notify
函数,执行指定任务。
下面我们实现这样一个案例:有4个任务,任务1、任务2、任务3、任务4;
任务3必须在任务2之后,任务4必须在前3个任务执行完毕后,才能执行,并且任务4需要主线程执行。
分析如下:
任务3必须在任务2之后,所以让这两个任务串行执行,同时,任务2和任务3整体可以和任务1并发执行,最后,任务4等待前三个任务执行完毕再执行,利用调度组实现如下:
案例1:
/**
* 调度组 dispatch_group_notify
*/
-(void)groupNotify{
// 获取全局并发队列
dispatch_queue_t globalQuene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建串行队列
dispatch_queue_t serialQuene = dispatch_queue_create("com.xxx.queue", DISPATCH_QUEUE_SERIAL);
// 创建调度组
dispatch_group_t group = dispatch_group_create();
NSLog(@"begin" );
// 将任务1提交到全局并发队列并关联调度组
dispatch_group_async(group, globalQuene, ^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1 ---%@",[NSThread currentThread]); // 打印当前线程
});
// 将任务2提交到串行队列并关联调度组
dispatch_group_async(group, serialQuene, ^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2 ---%@",[NSThread currentThread]); // 打印当前线程
});
// 将任务3提交到串行队列并关联调度组
dispatch_group_async(group, serialQuene, ^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3 ---%@",[NSThread currentThread]); // 打印当前线程
});
// 将任务4提交到主队列并关联调度组
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4 ---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---end");
});
NSLog(@"end");
}
打印如下:
begin
end
2 ---{number = 6, name = (null)}
1 ---{number = 7, name = (null)}
3 ---{number = 4, name = (null)}
4 ---{number = 1, name = main}
group---end
示例的结果也可能1234。但是dispatch_group_notify
的任务执行一定是在三个异步任务执行完毕之后才进行调用的,这和任务所属队列无关,也和dispatch_group_notify
所在的位置无关,即使我们将dispatch_group_notify
函数放到三个异步任务的前面,结果也是一样的。
dispatch_group_notify
函数会监听与调度组相关联的所有的任务执行完毕后才会执行自身任务。底层是通过信号量实现的。
- dispatch_group_enter / dispatch_group_leave
在上例中,我们通过dispatch_group_async
函数将任务提交到队列并且关联调度组。实际上,还有一种更加灵活的方式,就是dispatch_group_enter
和dispatch_group_leave
的搭配使用。
// 方式一:
dispatch_group_async(group, queue, ^{
// 。。。
});
// 方式二:
dispatch_group_enter(group);
dispatch_async(queue, ^{
//。。。
dispatch_group_leave(group);
});
从一定程度上,方式一和方式二是等价的。(dispatch_group_async
底层也是通过dispatch_group_enter
和dispatch_group_leave
实现的。)
dispatch_group_enter
: 标记为入组,执行一次,相当于组内待执行的任务数+1;
dispatch_group_leave
: 标记为出组,执行一次,相当于组内待执行的任务数-1;
当组内待执行的任务数为0时,会使dispatch_group_wait
解除阻塞,并回调dispatch_group_notify
函数。
我们将上面的案例换成dispatch_group_enter / dispatch_group_leave
方式如下:
案例2:
/**
* 调度组 dispatch_group_enter、dispatch_group_leave
*/
-(void)groupEnter_leave{
// 获取全局并发队列
dispatch_queue_t globalQuene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建串行队列
dispatch_queue_t serialQuene = dispatch_queue_create("com.xxx.queue", DISPATCH_QUEUE_SERIAL);
// 创建调度组
dispatch_group_t group = dispatch_group_create();
NSLog(@"begin" );
// 将任务1提交到全局并发队列并关联调度组
dispatch_group_enter(group);
dispatch_async(globalQuene, ^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1 ---%@",[NSThread currentThread]); // 打印当前线程
dispatch_group_leave(group);
});
// 将任务2提交到串行队列并关联调度组
dispatch_group_enter(group);
dispatch_async(serialQuene, ^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2 ---%@",[NSThread currentThread]); // 打印当前线程
dispatch_group_leave(group);
});
// 将任务3提交到串行队列并关联调度组
dispatch_group_enter(group);
dispatch_async(serialQuene, ^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3 ---%@",[NSThread currentThread]); // 打印当前线程
dispatch_group_leave(group);
});
// 将任务4提交到主队列并关联调度组
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4 ---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---end");
});
NSLog(@"end");
}
执行结果同上例是一致的。
在使用此方式实现的时候,一定要注意保证
dispatch_group_enter
和dispatch_group_leave
成对出现,不然dispatch_group_notify
可能无法回调。
- dispatch_group_wait
一旦调用dispatch_group_wait
函数,该函数就会处理调用的状态而不返回值,只有当
- 函数的currentThread停止
- 或到达wait函数指定的等待的时间
- 或Dispatch Group中的操作全部执行完毕
该函数才会返回值。也就是说,dispatch_group_wait
会一直等待,它会阻塞当前线程,直到上述条件的发生。
当指定timeout为DISPATCH_TIME_FOREVER时就意味着永久等待;
当指定timeout为DISPATCH_TIME_NOW时就意味不用任何等待即可判定关联于Dispatch Group的任务是否全部执行结束。
如果函数的返回值为0,意味着与调度组相关联的任务全部执行完毕。
如果函数的返回值非0,意味着在指定时间内,与调度组相关联的任务并没有全部执行完毕。
你可以对返回值进行条件判断以确定是否超出等待周期。
将案例1的代码加上dispatch_group_wait
函数如下:
/**
* 调度组 dispatch_group_notify
*/
-(void)groupNotify{
// 获取全局并发队列
dispatch_queue_t globalQuene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建串行队列
dispatch_queue_t serialQuene = dispatch_queue_create("com.xxx.queue", DISPATCH_QUEUE_SERIAL);
// 创建调度组
dispatch_group_t group = dispatch_group_create();
NSLog(@"begin" );
// 将任务1提交到全局并发队列并关联调度组
dispatch_group_async(group, globalQuene, ^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1 ---%@",[NSThread currentThread]); // 打印当前线程
});
// 将任务2提交到串行队列并关联调度组
dispatch_group_async(group, serialQuene, ^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2 ---%@",[NSThread currentThread]); // 打印当前线程
});
// 将任务3提交到串行队列并关联调度组
dispatch_group_async(group, serialQuene, ^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3 ---%@",[NSThread currentThread]); // 打印当前线程
});
// 将任务4提交到主队列并关联调度组
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4 ---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---end");
});
long time = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"time = %ld",time);
NSLog(@"end");
}
打印结果如下:
begin
2 ---{number = 6, name = (null)}
1 ---{number = 4, name = (null)}
3 ---{number = 6, name = (null)}
time = 0
end
4 ---{number = 1, name = main}
group---end
当timeout的值设为DISPATCH_TIME_FOREVER,意味着一直等待,dispatch_group_wait
会一直阻塞当前线程,直到与调度组相关联的任务全部执行完毕,所以当任务2、任务1、任务3完成后才继续执行主线程代码,打印的time值为0,代表关联的任务全部执行完毕。最后回调dispatch_group_notify
函数,执行任务4。
如果将time设为DISPATCH_TIME_NOW,打印的结果如下:
begin
time = 49
end
1 ---{number = 6, name = (null)}
2 ---{number = 3, name = (null)}
3 ---{number = 3, name = (null)}
4 ---{number = 1, name = main}
group---end
6.3 信号量:dispatch_semaphore
dispatch_semaphore
是基于mach内核的信号量接口实现的,信号量很复杂,因它建立在操作系统
的复杂性之上。不过我们可以用简单的方式去理解它,本质上,信号量是一个持有计数的信号,信号可以在线程之间互相传递,根据信号所持有的计数决定线程行为。
同时,信号量也很强大。
- 信号量可以控制线程并发访问的最大数量;
- 信号量可以保持线程同步,使异步任务同步化,让多个异步不同队列的线程串行执行;
- 信号量可以保证线程安全,给线程加锁;
信号量可以让你控制多个消费者对有限数量资源的访问。如果你创建了一个有着两个资源的信号量,那同时最多只能有两个线程可以访问临界区。其他想使用资源的线程必须在一个FIFO队列里等待。如果创建了一个只有一个资源的信号量,那么同一时刻只能有一个线程可以访问临界区。
信号量中常用的函数有:
dispatch_semaphore_create
:创建一个带有初始值的信号量
dispatch_semaphore_signal
:发送一个信号,让信号量+ 1
dispatch_semaphore_wait
:等待一个信号,让信号量-1,如果信号量小于0,根据设置的超时时间等待(阻塞所在线程)。
信号量是如何工作的呢?
我们先看看AFNetworking中信号量的应用
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
__block NSArray *tasks = nil;
// 初始化信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
tasks = dataTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
tasks = uploadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
tasks = downloadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
}
// 发信号
dispatch_semaphore_signal(semaphore);
}];
// 检验信号
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return tasks;
}
首先由dispatch_semaphore_create
函数创建一个带有初始值的信号量,也称发射信号量。
dispatch_semaphore_t
dispatch_semaphore_create(intptr_t value);
参数指定信号量的起始值。这个数字表示最多可以有多少线程访问临界区(注:这里初始值为0,也就是说,有人想使用信号量必然会被阻塞,直到有人增加信号量。)
由于这是一个异步任务,所以我们先不管异步子线程中的任务,直接主线程继续向下执行
intptr_t
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
等待一个信号,递减信号量的计数。如果信号量小于零,就会根据设置的超时时间等待(阻塞所在线程)。
这和调度组中
dispatch_group_wait
很相似,实际上,dispatch_group_wait
就是由信号量实现的。
这里初始的信号量值为0,进入dispatch_semaphore_wait
函数,信号量值减1,变为-1,小于0,此时的超时时间设置为DISPATCH_TIME_FOREVER,所以它会一直等待并阻塞所在线程,直到正确的接收到一个信号。
由于主线程被阻塞,此时我们关注异步子线程,子线程处理一定的任务后,进行了dispatch_semaphore_signal
函数的调用
intptr_t
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
dispatch_semaphore_signal
函数会发送信号,使信号量的计数+1。如果以前的值小于零,则这个函数将唤醒当前正在dispatch_semaphore_wait
这里等待的线程(如果一个线程被唤醒则返回非零,否则返回零。)
最后,主线程被唤醒,异步任务得以正确返回以供AFNetworking其他模块使用。
关于信号量的底层实现,参阅iOS多线程编程(五) GCD的底层原理。
6.4 一次执行函数:dispatch_once
dispatch_once
函数保证了代码在程序运行期间只执行一次,如果有这样的需求时,就可以使用dispatch_once
函数。通常,单例的创建就是借助此函数。
/**
* 一次执行函数: dispatch_once
*/
- (void)once {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
<#code to be executed once#>
// 只执行 1 次的代码(这里面默认是线程安全的)
});
}
6.5 延时执行函数:dispatch_after
如果需要指定任务在多少秒后执行,就可以使用dispatch_after
函数。那么在规定时间之后,任务就会被追加到主队列
中,需要注意的是,这个时间并不是绝对精准的。
/**
* 延时执行函数: dispatch_after
*/
- (void)after {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2.0 秒后异步追加任务代码到主队列,并开始执行
NSLog(@"after---%@",[NSThread currentThread]); // 打印当前线程
});
}
6.6 快速迭代函数:dispatch_apply
通常,我们使用for循环或者for...in函数进行遍历操作,GCD也提供了快速迭代方法,就是dispatch_apply
。
使用dispatch_apply
函数会按照指定的次数执行任务。之所以快速,就是利用了多线程”同时“执行的特性。
如果是在串行队列中使用dispatch_apply
,那么依然要按序一个个执行,这体现不出快速迭代的意义。如果在并发队列中进行异步执行
,那么就可以在多个线程同时异步遍历。
/**
* 快速迭代函数: dispatch_apply
*/
- (void)apply {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"apply---begin");
dispatch_apply(6, queue, ^(size_t index) {
NSLog(@"%zd---%@",index, [NSThread currentThread]);
});
NSLog(@"apply---end");
}
打印结果如下:
apply---begin
0---{number = 1, name = main}
1---{number = 5, name = (null)}
2---{number = 6, name = (null)}
3---{number = 4, name = (null)}
5---{number = 5, name = (null)}
4---{number = 1, name = main}
apply---end
可见,dispatch_apply
在并发队列中可以利用不同的线程执行任务,任务是无序的。
但是要注意:
不管是串行队列还是并发队列,apply---end都是在最后输出的,dispatch_apply
是同步
的,会等待全部任务执行完毕。