多线程(GCD)
GCD是异步执行任务的技术之一,通过Dispatch Queue来控制任务的执行,线程管理由系统实现,比以前更加有效率。
1.多线程编程
多线程原理
当mac或者iPhone打开一个app的时候,首先就将包含在应用程序中的CPU命令列配置到内存中,CPU从应用程序指定的地址开始,一个一个地执行CPU命令列。即便地址分散在各处,也是一条无分叉的路径。
image这里的“一个CPU执行的CPU命令列为一条无分叉路径”就是“线程”。
在1个CPU核工作下的多线程中,这个CPU可以“同时”执行多条不同路径上的命令,实现多线程,但基于CPU一个时间只能执行一个内存块的原则,CPU其实是每隔一段时间在不同路径上切换执行(被称为“上下文切换”),使看起来像同时在执行一样。
现在一个物理CPU芯片有64核(64个CPU),一台计算机会有多个CPU核同时在工作,这就不是看起来像同时在执行了,可能其他的CPU分担了一些路径执行。
多线程解决的事情:
简单来说:多线程可以保证应用程序的响应性能。
在iOS程序启动时,通过主线程绘制页面、响应触摸事件。如果在主线程中有耗时的操作,就会妨碍主线程(Runloop主循环)的执行,从而不能更新用户界面,画面停滞等问题。
image使用多线程,可以在长时间处理时,仍然能保证用户界面的响应性能。
多线程容易发生的问题:
-
多个线程更新相同的资源会导致数据不一致
-
停止等待时间的线程会导致多个线程相互持续等待(死锁)
-
使用太多线程会消耗大量内存
2.GCD原理
Dispatch Queue执行处理的等待队列。定义想执行的任务并追加到适当的Dispatch Queue中,Dispatch Queue按照追加的顺序(FIFO)执行处理。
-
Serial Dispatch Queue在一条队列上,按照FIFO的顺序执行追加的任务,后追加的任务需要等待前追加的执行完才能执行。
-
Concurrent Dispatch Queue由XNU内核管理多条任务链,不等待且不分顺序的执行任务。
同步 & 异步
-
同步(dispatch_sync)将指定任务追加到指定的队列中,并等待这个追加任务的结束。
-
异步(dispatch_async)将指定任务追加到指定的队列中,不等待这个追加任务的结束,继续向后执行。
Concurrent Dispatch Queue并发队列原理
iOS和OS X核心—— XNU内核决定应当使用的线程数,并只生成所需的线程数处理任务,当处理结束时,应当执行的处理数减少时,XNU内核会结束不再需要的线程。假如当Concurrent Dispatch Queue执行8个任务的时候,由于开辟线程会有内存小号,XNU并不一定会开8个线程在8条不同的队列上同时执行8个任务,而可能像下图这样用4个Concurrent Dispatch Queue用线程执行:
image执行顺序会根据处理内容和系统状态发生改变调整。
Serial Dispatch Queue串行队列原理
在执行顺序不能改变或不想并发执行任务的时候使用Serial Dispatch Queue,但是Serial Dispatch Queue比Concurrent Dispatch Queue能生成更多的线程,如果有1000个任务就会创建1000个Serial Dispatch Queue,会大量的占用系统的资源。
-
同步串行(dispatch_sync + Serial Dispatch Queue)
不新增线程
-
在当前串行队列下:线程暂停执行当前队列的任务(当前线程进入无限时间的等待直到新指派的任务完成),新指派的任务根据FIFO法则排在当前队列任务的后面(线程进入无限时间等待前一个任务执行结束),互相等待,引起线程死锁。
-
在新增串行队列下:暂停当前队列中的任务(当前队列进入无限时间的等待直到新指派的任务完成),新指派的任务在新的队列中,线程切换上下文到新队列执行新任务结束后回到原队列(等到结束),原队列线程继续执行原队列任务。
-
-
同步并发(dispatch_sync + Concurrent Dispatch Queue)不新增线程线程暂停执行当前队列的任务,由于并发型队列并行执行数量由内核XNU控制,新任务被派发到新队列中,当前线程切换上下文到新队列执行新派发的任务,执行结束后,切换回原队列。
-
异步串行(dispatch_async + Serial Dispatch Queue)新增线程
-
新增一条队列:新派发的任务被按FIFO的顺序放置在新增的这条队列中,在新增的多条线程中随机选择一条,依次执行队列上的任务,其余线程闲置,直到此队列被释放。
-
新增多条队列:新派发的任务被各自放置在不同的串行队列中,每个队列对应一条线程,相互执行没有时间顺序,效果等同于并发,但是性能不如并发,因为创建几条队列就会有几条线程数,数量过多时会影响系统性能,而并发是由XNU内核控制并调整线程数的。
-
-
异步并发(dispatch_async + Concurrent Dispatch Queue)新增线程,也新增队列并行执行的处理数量取决于当前的系统状态,即XNU内核基于队列中的处理数、CPU核数以及CPU负荷等当前系统的状态来决定并行的处理数,任务的执行顺序会根据处理内容和系统状态发生改变。
3.GCD使用
dispatch_release在iOS6.0\macOS10.8以后就不需要使用了
3.1 Dispatch Queue创建的dispatch queue对象名称为:dispatch_queue_t
创建方式:
//参数1 队列的名称,使用FQDN的命名方式,有助于后期的debug,会出现在程序崩溃时候的crashLog中
//参数2 串行队列为DISPATCH_QUEUE_SERIAL或NULL 并发队列为DISPATCH_QUEUE_CONCURRENT
dispatch_queue_t queueName = dispatch_queue_create("com.example.gcd.queueName",NULL);
3.2 Main Dispatch Queue && Global Dispatch Queue
-
Main Dispatch Queue:主线程所在的队列,只有一个,属于serial队列,更新UI等处理需要在此队列中执行
-
Global Dispatch Queue:全局队列,属于concurrent队列,有四个优先级
获取Main Dispatch Queue:
dispatch_queue_t mainQueue = dispatch_get_main_queue();
获取Global Dispatch Queue:
//global dispatch queue 的四个优先级
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
//参数1 优先级
//参数2 备用参数,一般填0
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
组合使用:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//并发处理一些耗时的任务
dispatch_async(dispatch_get_main_queue(), ^{
//回到主线程,做更新UI等操作
});
});
3.3 dispatch_set_target_queue
-
设置队列的优先级
-
建立队列的执行阶层
当使用dispatch_queue_create
创建队列的时候,不管是串行还是并行,它们的优先级都是DISPATCH_QUEUE_PRIORITY_DEFAULT
级别。
//设置队列的优先级
//参数1 被设优先级的队列
//参数2 目标队列
//将参数1的优先级设置成参数2的优先级
dispatch_set_target_queue(changedQueue, targetQueue);
//设置队列的执行阶层
//参数1 被指派的队列
//参数2 目标队列
//将changedQueue的任务指派到targetQueue中,如果targetQueue是串行,则changedQueue也是串行,反之亦然
//如果targetQueue是串行可以防止changedQueue并行执行
dispatch_set_target_queue(changedQueue, targetQueue);
3.4 dispatch_after
在指定的时间异步追加任务到指定队列,而不是指定时间执行任务。
//时间单位 NSEC:纳秒 USEC:微秒 SEC:秒 PER:每
#define NSEC_PER_SEC 1000000000ull //每秒有多少纳秒
#define NSEC_PER_MSEC 1000000ull //每毫秒有多少纳秒
#define USEC_PER_SEC 1000000ull //每秒有多少微秒
#define NSEC_PER_USEC 1000ull //每微秒有多少纳秒
//相对时间
//参数1:时间起点 DISPATCH_TIME_NOW现在 DISPATCH_TIME_FOREVER永久以后
//参数2:时间偏移量 ull是C语言的数值字面量 detal的单位是纳秒
dispatch_time_t time = dispatch_time(dispatch_time_t when, int64_t delta);
//绝对时间 (如2011年11月11日11时11分11秒)
//参数1:struct timespec类型的时间
dispatch_time_t wallTime = dispatch_walltime(const struct timespec * _Nullable when, int64_t delta);
//在time时间时,将block追加到queue中
dispatch_after(time, queue, ^{
});
绝对时间的使用方式:
//将NSDate对象转换成dispatch_time_t对象的方法
- (dispatch_time_t)getDispatchTimeByDate:(NSDate *)date
{
NSTimeInterval interval = date.timeIntervalSince1970;
double second, subsecond;
subsecond = modf(interval, &second);
struct timespec time;
time.tv_sec = second;
time.tv_nsec = subsecond * NSEC_PER_SEC;
dispatch_time_t walltime = dispatch_walltime(&time, 0);
return walltime;
}
3.5 dispatch_group
用来监听多个并发执行任务全部执行结束
//创建一个group
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, ^{});
dispatch_group_async(group, queue, ^{});
//当所有并发队列执行结束后,触发此方法,将任务追加到指定的队列
dispatch_group_notify(group, dispatch_get_main_queue(), ^{});
dispatch_group_wait
不仅能做到等待group中的并发任务结束,还能设置等待一定的时间。等待的原理就是执行dispatch_group_wait
的线程被暂停执行任务,当时间到或者group里的并发任务全部执行完毕,激活dispatch_group_wait
的执行线程并返回结果。而dispatch_group_notify
是简化dispatch_group_wait
使用的一种方式。
//用dispatch_group_wait实现等待一秒
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
//时间到或者group结束之前,线程被停止在这里,不往下执行。
long result = dispatch_group_wait(group, time);
//当时间到了,或者group出结果了,result得到结果
if(result == 0) {
//group执行结束
} else {
//时间到
}
3.6 dispatch_barrier_async
在并发队列中的等待追加的处理全部执行结束后串行追加到queue中,一般用来划分存取数据(排他控制),实现高效率数据库访问。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//任务被最先追加到队列中
dispatch_async(queue, ^{});
//执行dispatch_barrier_async的线程开始无限时等待,重新执行的信号是之前的queue中的任务都执行完毕
dispatch_barrier_async(queue, ^{});//开始执行时,任务被追加到串行队列,后续的任务开始排队
//在barrier任务执行结束后,后续任务才开始执行
dispatch_async(queue, ^{});
dispatch_group
和dispatch_set_target_queue
也能实现按序等待,但是源代码会很复杂。
3.8 dispatch_apply
在指定的队列上重复执行一定的次数,并等待全部处理执行结束。
形式上与dispatch_sync
和dispatch_group
组合效果一样,为了避免阻塞主线程,推荐在dispatch_async
中使用。
dispatch_queue_t globalQueue = dispatch_queue_create("com.example.gcd.queueName",DISPATCH_QUEUE_CONCURRENT);
//把当前线程喊停,往globalQueue上依次追加10个任务
dispatch_apply(10, globalQueue, ^(size_t index) {
NSLog(@"%zu",index);
});
//只有等10个任务全部处理结束了,当前线程才会继续往后走,中间这段时间线程阻塞
NSLog(@"done");
3.9 dispatch_suspend && dispatch_resume
-
dispatch_suspend:挂起已追加但是尚未执行的队列,如果已经开始执行的,就无法挂起了
-
dispatch_resume:恢复执行队列
//挂起队列
dispatch_suspend(queue);
//恢复队列
dispatch_resume(queue);
3.10 Dispatch Semaphore
比dispatch_barrier_async
处理更细粒度的排他控制,如果在多线程环境下往NSMutableArray中添加Object,,如果两个object几乎同时被加入,那就可能会出现,前一个object插入未完成,后一个就开始了。
信号量在多线程开发中被广泛使用,当一个线程在进入一段关键代码之前,线程必须获取一个信号量,一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待前面的线程释放信号量。
信号量的具体做法是:当信号计数大于0时,每条进来的线程使计数减1,直到变为0,变为0后其他的线程将进不来,处于等待状态;执行完任务的线程释放信号,使计数加1,如此循环下去。
3.11 dispatch_once
多线程下百分之百安全的做到只执行一次
//获取静态变量once
static dispatch_once_t once;
dispatch_once(&once, ^{
//里面的内容可以保证只被执行一次
});
网友评论