前言
Grand Central Dispatch
Grand Central Dispatch(GCD)是异步执行任务的技术之一。一般讲应用程序中记述的线程管理用的代码在系统级中实现,开发者只需要定义想执行的任务并追加到适当的Dispatch Queue 中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可以统一管理,也可执行任务,这样就比以前的线程更有效率。(摘自苹果的官方说明)
让我们看一下GCD之前,Cocoa框架提供的一些简单的多线程技术:1
2self performSelectorOnMainThread: withObject: waitUntilDone:
self performSelectorInBackground: withObject:
举个简单的列子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15-(void)doSomething{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
/*
耗时操作
*/
/*
耗时操作结束 调用主线程
*/
[self performSelectorOnMainThread:@selector(workDone) withObject:nil waitUntilDone:NO];
[pool drain];
}
-(void)workDone{
//回到主线程做事 比如UI刷新
}
关于performSelectorOnMainThread:方法中waitUntilDone参数的意义是这样的:如果传YES,则代表[pool drain]需要等待workDone结束之后才能执行,如果传NO,则代表不用等待,直接执行pool drain,再执行workDone。我们还注意到MRC下,需要手动管理内存,所以这里创建了一个自动释放池。
引入GCD之后,我们可以这么写:1
2
3
4
5
6
7
8
9
10
11dispatch_async(queue, ^{
/*
耗时操作
*/
/*
耗时操作结束 调用主线程
*/
dispatch_async(dispatch_get_main_queue(), ^{
//回到主线程做事 比如UI刷新
});
});
多线程编程
线程是什么?
我们知道一段代码大部分情况下是从上到下依次顺序执行的。那么如何保证其是依次执行的呢?首先编译器会将程序代码转为一长串的CPU命令列(就是通常我们理解的二进制代码),那么当应用程序启动的时候,CPU会从程序制定的位置开始,一个一个地执行CPU命令列。在if或者for语句中控制语句中,执行命令列的地址可能会是不连续的(即顺序不固定)。但是由于一个CPU一次只能执行一个命令,不能执行某处分开的并列的两个命令,因此通过CPU执行的CPU命令列就好比一条无分叉的大道,可能会来回绕弯,但是一定是单向的,其执行不会出现分歧。
这里所说的一个CPU执行的CPU命令列为一条无分叉路径即为线程。
多线程是什么?
现在一个物理的CPU芯片实际上有64个CPU(即64核),那么一个CPU核可以分为2个虚拟核心(比如因特尔超线程技术,把CPU的一个核心虚拟成2个 )。那么一台计算机上就可以使用多个CPU核来运行了,这种情况下,上文提到的无分叉路径就不止一条了,存在多条时即为多线程。
多线程编程是什么?
iOS的核心XNU内核在发生操作系统事件时会切换执行路径。执行中路径的状态,列入CPU的寄存器等信息保存到各自路径专用的内存块中。从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的CPU命令列,这就是上下文切换。
使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看上去就好像一个CPU内核可以能够并列的执行多个线程一样。而在具有多个CPU核的情况下,就不是看上去像了,而是真的提供了多个CPU核并行执行了多个线程的技术。这种利用多线程编程的技术就被称为多线程编程。
多线程编程的优缺点
缺点:多线程编程实际上是一种易发生各种问题的编程技术。比如多个线程更新相同的资源会导致数据的不一致数据竞争,停止等待事件的线程会导致多个线程相互持续等待死锁,使用太多线程会消耗大量内存等。
优点:保证应用程序的响应性能。应用程序在启动的时候,通过最先执行的线程,即”主线程”来描绘用户界面,处理触摸屏幕事件等。如果在主线程中进行长时间的处理,就会阻塞主线程的执行,即妨碍主线程中被称为RunLoop的主循环的执行,从而导致不能更新用户界面,应用程序的画面长时间卡顿,停滞等问题。而使用多线程编程,在执行长时间的处理时仍可以保证用户界面的响应性能。
GCD的API
Dispatch Queue:
Dispatch Queue是执行处理的等待队列。
苹果的官方说明:开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。用代码来解释:1
2
3
4
5dispatch_async(queue, ^{
/*
想执行的任务
*/
});
开发者通过用Block语法记述想执行的任务并将其追加到Dispatch Queue中,这样就可以使指定的任务在另一个线程中执行。Dispatch Queue按照追加的顺序FIFO执行处理。
Dispatch Queue分为两种
- 等待现在执行中处理的
Serial Dispatch Queue称为串行队列 - 不等待现在执行中处理的
Concurrent Dispatch Queue称为并发队列
假设现在分别在这两种队列中顺序追加了blk0,blk1,blk2,blk3这四个任务。那么在串行队列中,先执行blk0,blk0执行完毕以后才会执行blk1,blk1执行完毕之后才会执行blk2,依次执行下去,也就是说串行队列中的任务会按顺序执行且下一个任务总是在上一个任务执行完毕后开始执行。在并发队列中,先执行blk0,但是无论blk0的执行是否结束,都会开始执行后面的blk1,不管blk1的执行是否结束,都会开始执行后面的blk2,也就是说并发队列中的任务执行并不会等待上一个任务执行完毕。但是虽然并行队列中不用等待处理结束,可以并行执行多个处理,但并行执行的处理数量取决于当前系统的状态,即iOS内核基于Dispatch Queue中的处理数,CPU核数以及CPU负荷等当前运行系统的状态来决定的。所谓的并行执行,就是使用多个线程同时执行多个处理。
iOS的核心XUN内核会决定应当使用的线程数,并只生成所需的线程执行处理。另外,当处理结束,应当执行的处理数减少时,XUN内核会结束不再需要的线程。XUN通过Concurrent Dispatch Queue就可以完美的管理并行执行多个处理的线程。
串行队列的任务执行理解起来很简单,即等待执行。并发队列的任务执行稍微有点复杂,我们再来举个列子详细的说一下:
假设现在有4个线程在并发队列中等待任务的执行,当我们像并发队列中追加了6个任务后,首先blk0在线程0中开始执行,接着blk1在线程1中开始执行,blk2在线程2中开始执行,blk3在线程3中开始执行(因为当前队列中只有4个空闲的线程,所以一次性最多只能调用4个线程去执行任务)。我们假设blk0先执行完毕,那么此时线程0中没有任务执行了,线程0处于空闲的状态,此时队列会将blk4追加到线程0中执行。这个时候我们假设blk2在blk1之前执行完毕了,那么线程2空闲出来,队列会立即将blk5追加到线程2中执行。像这样在并发队列中执行任务处理时,执行顺序会根据处理内容和系统状态发生改变 它不同于执行顺序固定的串行队列。
dispatch_queue_create
我们现在知道了两种队列:串行和并发队列。那么怎么才能获取到这两种队列呢?
有两种方法:
第一种 通过GCD的API生成的Dispatch Queue:dispatch_queue_create
举例:1
2
3
4//生成串行队列
dispatch_queue_t MySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
//生成并行队列
dispatch_queue_t MyConcurrentDispatchQueue = dispatch_queue_create("com.example.gcd.MyConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_create 有两个参数,第一个参数是指定的队列的名称,苹果推荐Dispatch Queue的名称使用应用程序ID这种逆序全程域名的方式来命名,该命名会出现在Xcode的Instruments和CrashLog中,方便开发人员定位程序错误和问题。第二个参数在创建串行队列的时候直接传Null,创建并行队列的时候传DISPATCH_QUEUE_CONCURRENT。dispatch_queue_create 的返回值均为dispatch_queue_t类型来接收。
关于生成的线程数量。
对于串行队列,系统对一个已经追加任务处理的串行队列只会生成一个线程供其使用,因为串行队列的运行机制就是等待处理任务,不论你给它生成多少个线程,在串行队列中永远只会有一个在运行。假如现在需要100个任务同一时间处理,那么使用串行队列的话就需要创建100个串行队列去完成,这样就会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能。所以串行队列不应该被大量生成,往往我们只在考虑数据锁的情况下使用它:当多个线程更新相同的资源导致数据竞争的时候使用串行队列,这样可以保证数据安全,除此之外我们都应该使用并发队列去执行任务(不考虑系统的主线程更新UI)。
对于并发队列,因为XNU内核只使用有效管理的内核,因为不会出现串行队列这样的性能问题。
第二种 获取系统标准提供的Dispatch Queue
在程序启动的时候,系统提供了两个队列:Main Dispatch QueueGloble Dispatch Queue
Main Dispatch Queue是在主线程中执行的Dispatch Queue。因为主线程只有一个,所以Main Dispatch Queue实际上就是Serial Dispatch Queue (串行队列)。
追加到Main Dispatch Queue的处理是在主线程的Runloop中执行的。由于在主线程中执行,因此对于用户界面的更新操作必须是追加到Main Dispatch Queue中的。
Globle Dispatch Queue是所有应用程序都能够使用的Concurrent Dispatch Queue(并发队列)。通常我们并不需要额外创建一个并发队列来使用,直接获取Globle Dispatch Queue就可以了。
对于Globle Dispatch Queue来说,有4个执行优先级。分别是High Priority 高优先级Default Priority 默认优先级Low Priority 低优先级Background Priority 后台优先级
关于执行优先级的使用:在向Globle Dispatch Queue中追加处理时,应选择与处理内容对应的执行优先级的Globle Dispatch Queue。这里需要注意的一点是XNU内核用于Globle Dispatch Queue的线程并不能保证实时性。因此执行优先级只是大致的判断。
关于获取Main Dispatch Queue和Globle Dispatch Queue的方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/*
获取Main Dispatch Queue
*/
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
/*
Globle Dispatch Queue
*/
//高优先级
dispatch_queue_t globleDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
//默认优先级
dispatch_queue_t globleDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//低优先级
dispatch_queue_t globleDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
//后台优先级
dispatch_queue_t globleDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue
dispatch_queue_create 函数生成的Dispatch Queue(不管是串行还是并发队列),都使用与默认优先级Globle Dispatch Queue相同执行优先级的线程。当我们想变更其优先级的时候,就需要使用dispatch_set_target_queue函数了:1
2
3
4
5
6//需要改变优先级的队列
dispatch_queue_t MySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
//作为想改变优先级队列参考的队列
dispatch_queue_t globleDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
//设置优先级
dispatch_set_target_queue(MySerialDispatchQueue, globleDispatchQueueHigh);
这样,新生产的MySerialDispatchQueue本来优先级是默认优先级,通过dispatch_set_target_queue设置,其当前的优先级为High了。dispatch_set_target_queue方法的第一个参数为需要改变优先级的队列,第二个参数为优先级参考目标的队列。
通过dispatch_set_target_queue,我们还可以实现多个串行队列的并发执行。比如我们从多个Serial Dispatch Queue中,用dispatch_set_target_queue函数指定目标为某一个Serial Dispatch Queue。那么原本本应并行执行的的多个Serial Dispatch Queue,变成了只能同时处理一个任务。
dispatch_after
当我们想要将一个任务延期执行的时候,就可以用dispatch_after了。比如
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//三秒以后回到主线程更新UI
});
这里要注意的是,dispatch_after函数并不是在指定时间后执行处理,而只是在指定时间追加处理到Dispatch Queue。至于什么时候处理会执行,是根据当前的系统和队列状态来决定的。比如上面的代码,如果此时主线程没有其他任务在处理,根据Runloop的执行频率是1/60来看,处理最快是3秒钟执行,最慢是3+1/60秒执行。
Dispatch Group
写业务的时候,我们往往会碰到需要完成一些操作后,才能继续下一步的操作的情况。这个时候,如果放在串行队列中去完成的话,只需要将想执行的处理依次追加到串行队列中,并将下一步的操作放到最后追加。这样串行队列等待执行的机制就会保证业务逻辑的正确性。而在并发队列中,我们想要实现这种业务逻辑,就需要Dispatch Group了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15dispatch_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(@"1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"2");
});
dispatch_group_async(group, queue, ^{
NSLog(@"3");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"Done");
});
因为并行队列中的任务是不等待执行的,顺序不定。执行结果的顺序是不确定的,但Done一定是最后执行的。
dispatch_barrier_async
前面我们提到,使用并发队列进行数据读取和写入操作时,容易产生数据竞争的问题。而放在串行队列中就没有问题。写入处理确实不可与其他的写入处理以及包含读取处理的其他处理并行执行。但是如果只是读取处理与读取处理并发执行,在确保当前没有读取处理进行的情况下载串行队列中追加写入处理,那么就不会发生问题。
我们看看dispatch_barrier_async是如何应用的:1
2
3
4
5
6dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.ForBarrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_barrier_async(queue, blk_for_writing);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_barrier_async会等待追加到CONCURRENT Dispatch Queue上的并行执行的处理全部结束之后,再将指定的处理追加到该CONCURRENT Dispatch Queue中。然后再由dispatch_barrier_async函数追加的处理执行完毕后,CONCURRENT Dispatch Queue才恢复一般的动作,继续往下执行已经追加的处理。即,等blk0,1执行完毕后,blk_for_writing才会执行。blk_for_writing执行完毕后,blk2,3才会执行。
通过使用CONCURRENT Dispatch Queue和dispatch_barrier_async函数可以实现高效率的数据库访问和文件访问。
dispatch_sync和dispatch_async
dispatch_sync意味着将处理“非同步”的追加到队列中,无需等待。dispatch_async意味着将处理“同步”的追加到队列中,需要等待。
这里的等待意思就是当前线程停止。
关于dispatch_sync要注意的一点是不能在串行队列中同步追加处理,这样会造成死锁。很简单,串行队列在执行这些源代码,而源代码里面的操作需要等串行队列执行完源代码以后才能执行,这样相互等待就造成了死锁。
dispatch_apply
dispatch_apply函数按照指定的次数将指定的任务追加到指定的Dispatch Queue中,并等待全部处理执行结束。
举个列子:1
2
3
4
5
6NSArray *array = @[@"1",@"2",@"3",@"4",@"5"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply([array count], queue, ^(size_t index) {
NSLog(@"%@",array[index]);
});
NSLog(@"done");
该代码的执行顺序是不定的,但是done一定是最后才输出的。因为在全局队列中执行处理,是并发处理,所以1到5的打印顺序不固定。因为dispatch_apply函数会等待全部处理执行结束,所以 NSLog(@”done”)一定是最后才执行的。
方法的第一个参数是重复次数,第二个参数为追加对象的queue,第三个参数可以理解为索引。dispatch_apply因为与dispatch_sync函数一样会等待处理执行结束。所以推荐在dispatch_async函数中非同步的执行dispatch_apply函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14NSArray *array = @[@"1",@"2",@"3",@"4",@"5"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
dispatch_apply([array count], queue, ^(size_t index) {
NSLog(@"%@",array[index]);
});
//dispatch_apply函数处理全部结束 回到主线程 更新UI
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"done");
/*
界面更新等操作
*/
});
});
dispatch_suspend和dispatch_resume
当追加大量处理到队列中的时候,有的时候,我们希望暂停处理过程。这个时候调用dispatch_suspend可以挂起当前队列,当前正在执行的处理不会被停止,而尚未执行的处理会停止执行。当我们需要恢复处理过程的时候,调用dispatch_resume则会让尚未执行的已经停止的处理恢复执行。1
2dispatch_suspend(queue);
dispatch_resume(queue);
dispatch_semaphore
当并行执行的处理更新数据时,会产生数据不一致的情况,有时应用程序还会异常结束。虽然使用dispatch_barrier_async或者Serial Dispatch Queue函数可以避免这类问题,有必要进行更细粒度的排他控制。
dispatch_semaphore是持有技术的信号,该技术是多线程编程中的技术类型信号。所谓信号,类似于过马路时常用的手旗,当手旗是举起的时候代表你可以提供,放下手旗代表你不可以通过。对于dispatch_semaphore而言,使用技术实现该功能,即技术为0时等待处理,大于等于1的时候不等待。
dispatch_semaphore的生成函数,参数为技术值。1
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER),如果当前semaphore的技术为0时,改函数会永远等待。当semaphore的技术大于等于1时,dispatch_semaphore_wait会将semaphore的技数减一并返回。dispatch_semaphore_signal(semaphore),将semaphore的计数加一并返回。
举例1 现在我们不考虑顺序的将一些数据加入到数组中1
2
3
4
5
6
7
8
9dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *arrayM = [NSMutableArray array];
for (int i = 0; i<1000; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[arrayM addObject:[NSNumber numberWithInt:i]];
dispatch_semaphore_signal(semaphore);
});
首先,我们引用了一个全局队列。然后通过dispatch_semaphore_create()生成了一个信号量为1的信号。接下来,我们通过并发操作往数组里写入数据,为了保证写入操作的安全性,我们在每次写入操作之前,调用dispatch_semaphore_wait方法,因为semaphore的初始化信号量为1,所以走到这的时候,dispatch_semaphore_wait会通过执行并将信号量减一返回。等一次数据追加的操作完成后,调用dispatch_semaphore_signal使信号量加一。
这样一来的话,尽管有多个线程并发去写入数据,但一定是同步执行的。因为在第一个线程开始执行的时候,信号量就被减一变为0了,只要第一个线程执行没有结束,信号量就不会被加一。那么其他线程走到这里的时候,会因为信号量为0而永远等待。所以这种写入操作一定是线程安全的。
举例2 有的时候我们会碰到这样的业务逻辑,我们需要发起两个网络请求A,B。但是B一定需要在A请求回调之后再发送(最常见的token验证登录)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22-(void)loginButtonClick{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self verifyTokenAction:semaphore];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self loginSuccessAction];
}
-(void)verifyTokenAction:(dispatch_semaphore_t)semaphore{
NSURLSessionDataTask *task = [LoginSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (data) {
//token获取成功,发送信号量:
dispatch_semaphore_signal(semaphore);
}else{
//token获取错误,不发送信号量
}
}];
[task resume];
}
-(void)loginSuccessAction{
//登录成功
}
首先我们初始化一个信号量为0的信号,然后发送一个网络请求,接着调用dispatch_semaphore_wait,因为信号量为0,所以dispatch_semaphore_wait会一直等待处理,等到请求成功后,调用 dispatch_semaphore_signal使信号量加一,这个是时候因为信号量为1,dispatch_semaphore_wait会执行处理并将信号量减一返回。
关于dispatch_semaphore_create函数的参数我们知道是信号量的意思,可以利用信号量来控制并发线程的数量。比如我们生成了初始化信号量为n的信号,这个时候我们有n+10个任务需要处理。我们追加任务到并发队列中,那么信号会通过n个线程里的dispatch_semaphore_wait函数减一,即减n,信号量变为0,此时除非之前执行的n个处理中有结束的处理调用了dispatch_semaphore_signal函数使信号的信号量加一,否则信号量为0会永远等待执行。我们生成了初始化的信号量为3的信号,则代表最多只会有3个线程可以并发运行。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//任务1
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
任务1的处理
*/
dispatch_semaphore_signal(semaphore);
});
//任务2
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
任务2的处理
*/
dispatch_semaphore_signal(semaphore);
});
//任务3
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
任务3的处理
*/
dispatch_semaphore_signal(semaphore);
});
//任务4
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
任务4的处理
*/
dispatch_semaphore_signal(semaphore);
});
dispatch_once
dispatch_once函数是保证在应用程序执行中只执行一次指定处理的函数。如果不用dispatch_once的话,一般我们为了达到只创建一次的效果,我们会这么做:1
2
3
4
5
6
7static int hasBeenInitialized = NO;
if (hasBeenInitialized == NO) {
/*
初始化
*/
hasBeenInitialized = YES;
}
而使用dispatch_once的话,代码则更简洁。而且即使在多线程环境下执行,也可以保证绝对的数据安全。多用于单例的创建。1
2
3
4
5
6static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/*
初始化
*/
});