本文重在讨论苹果公司在IOS4引入的全新多线程编程技术——GCD。一个人理解来说一说GCD。
1. 引入GCD
首先在学习GCD之前,一定要对线程有一定的理解,例如,线程等待,死锁,线程同步等等,如需了解请点击【线程的介绍】。另外GCD是一中与Block有关的技术,因此还需要学习一下Block,请点击【IOS的Block】
好了,下面进入正文。GCD全称是"Grand Central Dispatch",译为大中枢派发(来自《Effective Objective-C》)。GCD对线程进行了抽象,开发者只需要定义想要执行的任务(Block)并追加到合适的任务队列(Dispatch Queue)中,GCD就能生成必要的线程并执行任务。这里要着重的注意:任务(Block) + 任务队列(Dispatch Queue)。GCD用一种非常简洁的方式来实现了负责的多线程编程。
很多IOS的资料都会推荐使用GCD,因为使用GCD会带来如下好处:
- GCD 可用于多核的并行运算;
- GCD 会自动利用更多的 CPU 内核(比如双核、四核);
- GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程);
- 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。
上述的好处会在后面的介绍中一一体现。
2. GCD的任务与队列
2.1 任务
任务就是要执行的Block,其类型为dispatch_block_t
。如果查看定义其实一个无返回值无参数的Block:typedef void (^dispatch_block_t)(void);
。
2.2 队列
队列就是数据结构中满足先进先出(FIFO)特性的队列,不过该队列中存储的数据是代码块,其类型为dispatch_queue_t
。任务队列分为两种:
-
DISPATCH_QUEUE_SERIAL,串行队列
-
DISPATCH_QUEUE_CONCURRENT,并行队列
对于上述两种队列。如果对于串行和并行有一定了解的话,就能明白。串行队列中,任务的执行是按部就班的一个一个的执行;并行队列中,任务的执行是根据当前系统的状况,创建新线程或使用空闲线程来取到队列中的任务并执行。因此串行队列中就只有一个线程来执行任务,而并行队列中会有多个线程来并行执行。
ps:并行是一个相对的概念,如果当前系统不是多核的,那么就不支持同一时间执行多个任务,因此就不支持并行
3. GCD的基本使用
基本的介绍就在前面结束,下面通过介绍API来进一步了解和使用。
3.1 创建队列
dispatch_queue_create(const char *_Nullable label,dispatch_queue_attr_t _Nullable attr);
第一个参数是队列的名字,通常采用公司域名的逆置,例如:com.xxx.xxx.queue或者团队的逆置名。第二个参数是队列的类型DISPATCH_QUEUE_CONCURRENT
和DISPATCH_QUEUE_SERIAL
。返回值类型为dispatch_queue_t
,即就是返回一个队列
3.2 任务的追加
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
async是asynchronous的简写,译为非同步,反之就是同步sync。第一个参数是任务队列,第二个参数是要执行的Block
-
dispatch_sync
,往一个派发队列上提交一个同步执行的Block,阻塞当前线程.不开新线程. -
dispatch_async
,往一个派发队列上提交一个异步执行的Block,不阻塞当前线程.可能会开新线程.
3.3 队列的获取
dispatch_get_main_queue()
获取主线程队列。对于串行队列,GCD 默认提供了主队列(Main Dispatch Queue)。主队列其实并不特殊。 主队列的实质上就是一个普通的串行队列,只是因为默认情况下,当前代码是放在主队列中的,然后主队列中的代码,又都会放到主线程中去执行,所以才造成了主队列特殊的现象
dispatch_get_global_queue(long identifier, unsigned long flags);
获取全局队列。对于并行队列,GCD 默认提供了全局并行队列(Global Dispatch Queue)。第一个参数表示队列优先级,一般用 DISPATCH_QUEUE_PRIORITY_DEFAULT
。第二个参数为保留字段备用(一般为0)
3.3 组合
GCD的基本使用,这里不再介绍,因为看到前面的API就可以使用了。这里讨论一下注意事项。首先,前面提到队列有两种队列,如果加上后面GCD默认提供的队列则有 4 种,但是全局并行队列可以作为普通并行队列来使用。由于当前代码默认放在主队列中,在使用中,会有差异,所以主队列很有必要专门来研究一下。由于追加又分为异步追加和同步追加,因此总共有 6 中组合方式。
- 同步执行 + 并行队列
- 异步执行 + 并行队列
- 同步执行 + 串行队列
- 异步执行 + 串行队列
dispatch_sync
提交的Block因为优化的原因几乎总是在当前线程执行的(注意是当前线程
),也就是说在哪个线程调用的这个方法,那么该Block就在哪个线程上执行,唯一的一个例外就是如果在子线程中调用并提交到主队列则block是在主线程执行。之所以说这个,是因为同步执行在某些情况下会导致死锁就是这个原因。
3.3.1 同步执行 + 并行队列
在当前这一个线程中执行任务,不具有派发的能力,当前线程执行完一个任务,再执行下一个任务。
任务按顺序执行的。按顺序执行的原因:虽然 并行队列
可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务
不具备开启新线程的能力),所以也就不存在并行。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务
需要等待队列的任务执行结束)。所以任务只能一个接一个按顺序执行,不能同时被执行。
3.3.2 异步执行 + 并行队列
异步执行
具备开启新线程的能力。且 并行队列
可开启多个线程,同时执行多个任务
3.3.3 同步执行 + 串行队列
同步执行
不具备开启新线程的能力,串行队列
每次只有一个任务被执行,任务一个接一个按顺序执行。此时就会带来问题。如在串行队列中取到一个block中,有一个执行了sync,追加了一个block2,且追加到当前的queue。那么sync会阻塞当前线程,直到block2执行完毕。但是线程被阻塞,block2永远不会被执行,因此死锁了。
概括一下就是:在当前queue上调用sync函数,sync指定的queue也是当前queue。需要执行的block被放到当前queue的队尾等待被执行,因为这是一个串行的queue,调用sync函数会阻塞当前队列,等待block被执行->这个block一直不会被执行->sync函数一直不返回,所以当前queue就被阻塞了,造成了死锁。
3.3.4 异步执行 + 串行队列
异步执行
具备开启新线程的能力,串行队列
只开启一个线程,串行队列
每次只有一个任务被执行,任务一个接一个按顺序执行