一、关于多线程
1、并行与并发
- 并发为逻辑上的同时发生。在多线程操作时,如果系统是单核CPU,则它根本不可能同时进行一个以上的线程,它只能把CPU运行时间划分成极短个时间段,再将时间段分配给各个线程执行。这样CPU快速的在不同的线程之间切换,由于时间间隔较短,使人感觉多个任务在同时运行。
- 并行为物理(实际)上的同事发生。在多核CPU中。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行。
2、资源共享和资源饥饿
多线程编程中许多问题的根源就是在于多线程中访问共享资源。在多线程中任何一个共享的资源都可能是一个潜在的冲突点。
- 数据竞争
多个线程更
新相同资源时会导致数据竞争,可以通过互斥锁来解决数据竞争的问题。但是使用锁会严重影响程序性能,GCD提供了更为高校的方式Serial Dispatch Queue。
- 原子性
从语言层面来说,在OC中
将属性以 atomic 的形式来声明,就能支持互斥锁了。事实上在默认情况下,属性就是 atomic 的。将一个属性声明为 atomic 表示每次访问该属性都会进行隐式的加锁和解锁操作。虽然最把稳的做法就是将所有的属性都声明为 atomic,但是加解锁这也会付出一定的代价。
- 死锁
当多个线程在相互等等着对方结束时,就会发生死锁,这时程序的执行会被卡住。GCD中使用
dispatch_sync 时就有可能发生死锁。
- 资源饥饿
锁定的共享资源会引起读写问题。大多数情况下,限制资源一次只能有一个线程进行读取访问其实是非常浪费的。因此,在资源上没有写入锁的时候,持有一个读取锁是被允许的。在这种情况下,如果一个持有读取锁的线程在等待获取写入锁的时候,其他细纹读取资源的线程则因为无法获得这个读取锁而导致资源饥饿的发生。
3、优先级反转
优先级反转是指程序在运行时低优先级的任务阻塞了高优先级的任务,有效的反转了任务的优先级。GCD提供了4种级别的优先级队列,分别是Default, High, Low, Background。 高优先级和低优先级的任务之间共享资源时,就可能发生优先级反转。当低优先级的任务获得了共享资源的锁时,该任务应该迅速完成,并释放掉锁,这样高优先级的任务就可以在没有明显延时的情况下继续执行。然而高优先级任务会在低优先级的任务持有锁的期间被阻塞。如果这时候有一个中优先级的任务(该任务不需要那个共享资源),那么它就有可能会抢占低优先级任务而被执行,因为此时高优先级任务是被阻塞的,所以中优先级任务是目前所有可运行任务中优先级最高的。此时,中优先级任务就会阻塞着低优先级任务,导致低优先级任务不能释放掉锁,这也就会引起高优先级任务一直在等待锁的释放。使用不同优先级的多个队列听起来虽不错,但毕竟是纸上谈兵。它让本来就复杂的并行编程变得更加复杂和不可预见。因此我们写代码的时候最好只用Default优先级的队列,不要使用其他队列来让问题复杂化。
二、GCD的API
1、 Dispatch Queue
GCD为我们提供了两种Dispatch Queue,Serial Dispatch Queue 和 Concurrent Dispatch Queue。
- Concurrent Dispatch Queue(并发队列)
任务以FIFO从队列中移除,然后并发运行,可以按照任何顺序完成。它会自动开启多个线程同时执行任务,
同时执行的任务执行在不同的线程之上由调度队列管理。同时执行的任务数目(使用的线程数)取决于系统的条件。
即iOS和OS X基于Dispatch Queue中的处理数、CPU核数以及CPU负荷等当前系统的状态来决定Concurrent Dispatch Queue中并行执行的处理数。iOS和OS X的核心—XNU内核决定应当使用的线程数,并只生成所需的线程执行处理。另外,当处理结束时,应当执行的处理数减少时,XNU内核会结束不再需要的线程。这样我们仅使用Concurrent Dispatch Queue便可以完美地管理并行执行多个处理的线程。
可以通过以下代码来创建Concurrent Dispatch Queue
dispatch_queue_t
concurrentQueue =
dispatch_queue_create
(
"com.example.ConcurrentQueue"
,
DISPATCH_QUEUE_CONCURRENT
);
- Serial Dispatch Queue (串行队列)
任务以FIFO从序列中一个一个执行。一次只调度一个任务,队列中的任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)而且只会开启一条线程。需要注意的是:一旦生成了Serial Dispatch Queue并添加了任务,系统对于一个Serial Dispatch Queue就只生成并使用一个线程。如果生成2000个Serial Dispatch Queue并添加任务,那么就生成2000个线程。如果过多的使用多线程,就会消耗大量的内存,引起CPU不停的在不同的线程之间切换,大幅降低系统的响应性能。合理的做法是———只在多个线程更新相同的资源导致数据竞争时使用Serial Dispatch Queue。
可以通过以下代码来创建Serial Dispatch Queue
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.SerialQueue", NULL);
需要注意的是:哪怕是在ARC环境下,通过 dispatch_queue_create 创建的Dispatch Queue必须由程序员负责释放,通过dispat_release可以释放创建的dispatch queue。
- Main Dispatch Queue (主队列)
Main Dispatch Queue是主线程任务的执行队列,追加到Main Dispatch Queue的任务将在主线程中执行。Main Dispatch Queue是一个特殊的串行队列。
- Global Dispatch Queue (全局队列)
Global Dispatch Queue是GCD提供的并发队列。执行并发任务时,没有必要通过dispatch_queue_create生成并发队列,只要获取Global Dispatch Queue即可。另外,Global Dispatch Queue有四个执行优先级:高优先级、默认优先级、低优先级和后台优先级。
由于main Dispatch Queue和Global Dispatch Queue由系统创建和维护,对两者执行dispatch_retain函数和dispatch_release函数不会引起任何变化也不会有任何问题。
开发中常用的Dispatch Queue的种类
名称 | Dispatch Queue 的种类 | 说明 |
Main Dispatch Queue | Serial Dispatch Queue | 主线程执行(系统提供) |
Global Dispatch Queue(High Priority) | Concurrent Dispatch Queue | 执行优先级高(系统提供) |
Global Dispatch Queue(Default Priority) | Concurrent Dispatch Queue | 执行优先级默认(系统提供) |
Global Dispatch Queue(Low Priority) | Concurrent Dispatch Queue | 执行优先级低(系统提供) |
Global Dispatch Queue(Background Priority) | Concurrent Dispatch Queue | 执行优先级后台(系统提供) |
用户创建的Serial Dispatch Queue | Serial Dispatch Queue | 用于避免数据竞争问题(手动创建) |
2、Dispatch_sync/Dispatch_async
- Dispatch_async 对于`dispatch_async`来说,把block(任务)提交到队列,立刻返回执行下一步.不等待block执行完毕.
- Dispatch_sync 对于`dispatch_sync`来说,把block(任务)提交到队列,不立刻返回,它等待提交到队列的block执行完毕才继续向下执行.
这里需要注意的是使用
Dispatch_sync的过程中必须小心。因为需要等待任务执行完毕才会返回所以稍有不慎就会导致死锁。这里有两个典型的死锁案例:
dispatch_queue_t
queue =
dispatch_get_main_queue
();
dispatch_sync (queue, ^{
NSLog ( @"Hello World!" );
});
dispatch_sync (queue, ^{
NSLog ( @"Hello World!" );
});
该源代码在主线程中执行指定的Block,并等待其执行结束。而实际上主线程正在执行这些源码,所以无法执行追加到Main Dispatch Queue中的Block。
dispatch_queue_t
queue =
dispatch_queue_create
(
"com.example.queue"
,
NULL
);
dispatch_async (queue, ^{
dispatch_sync (queue, ^{
NSLog ( @"Hello World!" );
});
});
dispatch_async (queue, ^{
dispatch_sync (queue, ^{
NSLog ( @"Hello World!" );
});
});
该源代码也是一样的子线程需要执行并等待通过dispatch_sync提交的Block执行结果。而时间上子线程正在执行这些源码,所以无法执行追加到子线程中的Block。
3、dispatch_after
对于dispatch_after需要注意的是:dispatch_after并不是在指定时间后执行处理,而是在指定时间后将任务追加至dispatch_queue。
4、Dispatch_Group(调度组)
调度组是GCD提供的阻塞线程直至一个或多个任务执行完毕的一种方式。当一个任务的执行依赖于其他一个或多个任务执行完毕的时候我们可以使用调度组。例如,我们需要在调度若干任务来计算一些数据之后,去做一些操作。我们可以使用调度组来组织这些任务,拿到所有任务执行结果后执行后续的操作。
代码示例:
dispatch_queue_t
queue =
dispatch_get_global_queue
(
DISPATCH_QUEUE_PRIORITY_DEFAULT
,
0
);
dispatch_group_t group = dispatch_group_create ();
dispatch_group_async (group, queue, ^{
NSLog ( @"BLK0" );
});
dispatch_group_async (group, queue, ^{
NSLog ( @"BLK1" );
});
dispatch_group_async (group, queue, ^{
NSLog ( @"BLK2" );
});
dispatch_time_t time = dispatch_time ( DISPATCH_TIME_NOW , 1 ull * NSEC_PER_SEC );
long result = dispatch_group_wait (group, time);
if (result == 0 ) {
dispatch_group_notify (group, queue, ^{
NSLog ( @"Done" );
});
} else {
NSLog ( @" 超时 " );
}
dispatch_group_t group = dispatch_group_create ();
dispatch_group_async (group, queue, ^{
NSLog ( @"BLK0" );
});
dispatch_group_async (group, queue, ^{
NSLog ( @"BLK1" );
});
dispatch_group_async (group, queue, ^{
NSLog ( @"BLK2" );
});
dispatch_time_t time = dispatch_time ( DISPATCH_TIME_NOW , 1 ull * NSEC_PER_SEC );
long result = dispatch_group_wait (group, time);
if (result == 0 ) {
dispatch_group_notify (group, queue, ^{
NSLog ( @"Done" );
});
} else {
NSLog ( @" 超时 " );
}
这里需要注意的是:可以通过dispatch_group_wait来设置超时时间,如果返回值为0,则代表在指定时间内所有操作执行完毕。否则就意味着虽然经过了指定的时间但属于调度组的某一个操作还没有执行完毕。如果不使用dispatch_group_wait指定超时时间或将超时时间设为DISPATCH_TIME_FOREVER,则调度组会在所有操作执行完毕后执行dispatch_group_notify的回调任务。
5、dispatch_apply
dispatch_apply函数是dispatch_synv函数和Dispatch Group的关联API。该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等待全部处理执行结束。
代码示例:
dispatch_queue_t
queue =
dispatch_get_global_queue
(
DISPATCH_QUEUE_PRIORITY_DEFAULT
,
0
);
dispatch_apply ( 10 , queue, ^( size_t index) {
NSLog ( @"%zu" ,index);
});
NSLog ( @"done" );
dispatch_apply ( 10 , queue, ^( size_t index) {
NSLog ( @"%zu" ,index);
});
NSLog ( @"done" );
执行结果如下:
2
0
1
3
4
5
8
7
9
done
0
1
3
4
5
8
7
9
done
6、dispatch_suspend/dispatch_resume
当追加大量处理到Dispatch Queue时,在追加处理的过程中,有时希望不执行已追加的处理。在这种情况下,只要挂起Dispatch Queue即可。当可以执行时再恢复。
dispatch_suspend函数挂起指定的Dispatch Queue。
dispatch_resume函数恢复指定的Dispatch Queue。