iOS多线程之GCD、GCD处理多任务的网络请求、多读单写
在软件开发中使用多线程可以大大地提高用户体验,提高效率。Grand Central Dispatch(CGD)则是C语言的一套多线程开发框架,相比NSThread和NSOperation,GCD更加高效,并且线程由系统管理,会自动运行多核运算。因为这些优势,GCD是Apple推荐给开发者使用的首选多线程解决方案。
1、GCD的调度机制
GCD框架中一个很重要的概念是调度队列,我们对线程的操作实际上是由调度队列完成的。我们只需要将要执行的任务添加到合适的队列中即可。在GCD框架中,有如下三种类型的调度队列。
1.1主队列
其中的任务在主线程中执行,因为其会阻塞主线程,所以是一个串行的队列。可以通过下面的方法得到:
dispatch_get_main_queue();
1.2全局并行队列
队列中任务的执行严格按照先进先出的模式进行。如果是串行的队列,则当一个任务结束后,才会开启另一个任务,如果是并行队列,则任务的开启顺序和添加顺序是一致的。系统为iOS应用自动创建了4个全局共享的并发队列。使用下面的函数获得:
dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>);
上面函数的第一个参数是这个队列的ID,系统的4个全局队列默认的优先级不同,这个参数可填写的定义如下:
#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//后台的全局队列,优先级别最低
这个函数的第二个参数是一个预留参数,我们可以传NULL.
1.3自定义队列
上面的两种队列都是系统为我们创建好的,我们只需要获取到他们,添加任务即可。当然我们也可以创建自己的队列,包含串行和并行的。使用如下方法来创建:
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
其中第一个参数是这个队列的名字,第二个参数决定创建的是串行还是并行队列。填写DISPATCH_QUEUE_SERIAL或NULL创建串行队列,填写DISPATCH_QUEUE_CONCURRENT创建并行队列。
2、添加任务到调度队列中
使用dispatch_sync(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)函数或者dispatch_async(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)函数来同步或异步的执行任务。示例如下:
- (void)creatGCDQueue {
//创建一个串行的队列
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
//向队列中添加同步任务1
dispatch_sync(queue, ^{
NSLog(@"%@:task1",[NSThread currentThread]);
});
//向队列中添加异步任务2
dispatch_async(queue, ^{
NSLog(@"%@:task2",[NSThread currentThread]);
});
}
//打印信息:
image.png
上面的代码创建了一个串行的自定义队列,并且向队列中添加了一个同步的任务和一个异步的任务。需要注意,这里的同步和异步指的是针对当前代码运行所在的线程而言的。
从打印信息可以看出,同步的任务是在主线程中执行,异步的任务是在单独的线程中执行,由于我们创建的调度队列是串行的,因此先开启了任务1,后开启了任务2.
只有当调度队列是并行,而且向队列中添加的任务也是异步的时候,多任务才会实现并行异步执行。
实现如下:
- (void)creatGCDQueue {
//创建一个并行的队列
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
//向队列中添加异步任务1
dispatch_async(queue, ^{
for (int i = 0; i < 15; i ++) {
NSLog(@"%@ = %d:task1",[NSThread currentThread],i);
}
});
//向队列中添加异步任务2
dispatch_async(queue, ^{
for (int i = 0; i < 15; i ++) {
NSLog(@"%@ = %d:task2",[NSThread currentThread],i);
} });
}
3、使用队列组
通过前面的学习,我们现在已经可以运用队列多线程执行任务了,但是GCD的强大之处远远不止如此。看下面的例子。
如果有3个任务A、B、C,其中A与B是没有关系的,他们可以并行执行,C必须是A、B都结束之后才能执行,当然,实现这样的逻辑并不困难,使用KVO就可以实现,但是如果使用队列处理这样的逻辑,则代码会更加清晰简单。
可以使用dispatch_group_create()创建一个队列组,使用如下函数将队列添加到队列组中:
void dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
队列中的队列是异步执行的,示例如下:
- (void)creatGCDGroup {
//创建一个队列组
dispatch_group_t group = dispatch_group_create();
//创建一个异步队列
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
//添加任务
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 10; i ++) {
NSLog(@"%@ = %d:task1",[NSThread currentThread],i);
}
});
//添加任务
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 10; i ++) {
NSLog(@"%@ = %d:task2",[NSThread currentThread],i);
}
});
//阻塞线程,直到前面的队列任务执行完成
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
for (int i = 0; i < 10; i ++) {
NSLog(@"%@ = %d:over",[NSThread currentThread],i);
}
}
打印结果如下:
image.png
以上代码完美的实现了我们的任务依赖需求,可以看出GCD的强大了吧,复杂的任务逻辑关系因为GCD变得十分清晰简单。
4、GCD对循环任务的处理
说到循环,除了常规的while循环,for循环外,for-in也是开发中常用的一种循环方式。for-in循环通常来进行数组或字典的遍历,这种遍历通常不关心循环执行的顺序。使用GCD,配合设备的多核运算技术,我们可以将这种循环遍历的性能提升到极致,示例如下:
- (void)creatGCDApply {
dispatch_apply(20, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t i) {
NSLog(@"%@:%zu",[NSThread currentThread],i);
});
}
打印信息如下:
image.png
从打印信息可以看出,循环是由多个不同的线程完成的,比如我们的设备是8核的CPU。因此每个线程单独在一个核执行,这将循环的运行效率提升到了极致。大大提高了运行速率。
5、GCD中的消息与信号
5.1Dispatch Source
在GCD框架中提供了dispatch_source_t类型的对象,dispatch_source_t类型的对象可以用来传递和接收某个消息。在任一线程上调用它的一个函数 dispatch_source_merge_data 后,会执行 Dispatch Source 事先定义好的句柄(可以把句柄简单理解为一个 block )。
这个过程叫 Custom event ,用户事件。是 dispatch source 支持处理的一种事件。简单地说,这种事件是由你调用 dispatch_source_merge_data 函数来向自己发出的信号。
示例如下:
- (void)creatGCDSource {
//创建一个数据对象,DISPATCH_SOURCE_TYPE_DATA_ADD的含义表示当数据变化时相加
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
//设置响应分派源事件的block,在分派源指定的队列上运行
dispatch_source_set_event_handler(source, ^{
NSLog(@"%lu:sec",dispatch_source_get_data(source));//得到分派源的数据
dispatch_async(dispatch_get_main_queue(), ^{
//更新UI
});
});
//启动
dispatch_resume(source);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//网络请求
//向分派源发送事件,需要注意的是,不可以传递0值(事件不会被触发),同样也不可以传递负数。
dispatch_source_merge_data(source, 1);
});
}
注意:DISPATCH_SOURCE_TYPE_DATA_ADD是将所有触发结果相加,最后统一执行响应,但是加入sleepForTimeInterval后,如果interval的时间越长,则每次触发都会响应,但是如果interval的时间很短,则会将触发后的结果相加后统一触发。这在更新UI时很有用,比如更新进度条时,没必要每次触发都响应,因为更新时还有其他的用户操作(用户输入,触碰等),所以可以统一触发
比如我们写一个进度条的示例:
- (void)creatGCDSource {
//1、指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)。设定Main Dispatch Queue 为追加处理的Dispatch Queue
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
__block NSUInteger totalComplete = 0;
dispatch_source_set_event_handler(source, ^{
//当处理事件被最终执行时,计算后的数据可以通过dispatch_source_get_data来获取。这个数据的值在每次响应事件执行后会被重置,所以totalComplete的值是最终累积的值。
NSUInteger value = dispatch_source_get_data(source);
totalComplete += value;
NSLog(@"进度:%@", @((CGFloat)totalComplete/100));
NSLog(@":large_blue_circle:线程号:%@", [NSThread currentThread]);
});
//分派源创建时默认处于暂停状态,在分派源分派处理程序之前必须先恢复。
dispatch_resume(source);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//2、恢复源后,就可以通过dispatch_source_merge_data向Dispatch Source(分派源)发送事件:
dispatch_async(queue, ^{
for (NSUInteger index = 0; index < 100; index++) {
dispatch_source_merge_data(source, 1);
NSLog(@":recycle:线程号:%@~~~~~~~~~~~~i = %ld", [NSThread currentThread], index);
sleep(0.1);
}
});
}
5.2、信号量 singer
信号量是GCD中一个很重要的概念,他的用法与消息的传递有所类似,其本示例代码如下:
- (void)creatGCDSinger {
//创建一个信号,其中的参数是信号的初始值
dispatch_semaphore_t singer = dispatch_semaphore_create(0);
//发送信号,信号量+1
dispatch_semaphore_signal(singer);
//等待信号,当信号量大于0时,执行后面的代码,否则等待,第二个参数为等待的超时时长,下面设置的为一直等待
dispatch_semaphore_wait(singer, DISPATCH_TIME_FOREVER);
NSLog(@"singer");
}
注意,dispatch_semaphore_wait函数会阻塞当前线程,在主线程中要慎用。通过发送信号函数:dispatch_semaphore_signal(),可以使信号量+1,每次执行过等待信号后,信号量会-1,如此,我们可以很方便地控制不同队列中方法的执行流程。
5.2.1限制线程的最大并发数
- (void)creatGCDSinger {
//创建一个信号,其中的参数是信号的初始值
dispatch_semaphore_t singer = dispatch_semaphore_create(2);
for (int i = 0; i < 15; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//等待信号,当信号量大于0时,执行后面的代码,否则等待,第二个参数为等待的超时时长,下面设置的为一直等待
dispatch_semaphore_wait(singer, DISPATCH_TIME_FOREVER);
//doing
sleep(1);
//发送信号,信号量+1
dispatch_semaphore_signal(singer);
});
}
}
如上述代码可知,总共异步执行15个任务,但是由于我们设置了值为2的信号量,每一次执行任务的时候信号量都会先-1,而在任务结束后使信号量加1,当信号量减到0的时候,说明正在执行的任务有2个,这个时候其它任务就会阻塞,直到有任务被完成时,这些任务才会执行。
注意,信号量的正常的使用顺序是先降低(dispatch_semaphore_wait)然后再提高(dispatch_semaphore_signal),这两个函数通常成对使用。
5.2.2阻塞发请求的线程
有些时候,我们需要阻塞发送请求的线程,比如在多个请求回调后统一操作的需求,而这些请求之间并没有顺序关系,且这些接口都会另开线程进行网络请求的。一般地,这种多线程完成后进行统一操作的需求都会使用队列组(dispatch_group_t)来完成,但是由于是异步请求,没等其异步回调之后,请求的线程就结束了,为此,就需要使用信号量来阻塞住发请求的线程。实现代码如下:
- (void)creatGCDSinger {
//创建线程组
dispatch_group_t group = dispatch_group_create();
//获取队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//任务1
dispatch_group_async(group, queue, ^{
//请求1
[self request1];
});
//任务2
dispatch_group_async(group, queue, ^{
//请求2
[self request2];
});
//任务3
dispatch_group_async(group, queue, ^{
//请求3
[self request3];
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"-------所有网络请求已请求完成-------");
});
}
- (void)request1 {
//创建信号量,并设置为0,信号量本质是资源数,为0表示用完,需要等待
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
//模拟网络请求-异步
//每次网络请求成功或失败后,都让信号量+1,表示释放当前资源,其他线程可以抢占了
[[KNetRequestManager share] getSomeData:^{
//网络请求成功,发送信号
dispatch_semaphore_signal(sema);
} errorBlock:^{
//网络请求失败,发送信号
dispatch_semaphore_signal(sema);
}];
//如果信号量为0,表示没有资源可用,便一直等待,不再往下执行.只有当网络请求成功或失败时,才会往下走
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
- (void)request2 {
//创建信号量,并设置为0,信号量本质是资源数,为0表示用完,需要等待
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
//模拟网络请求-异步
//每次网络请求成功或失败后,都让信号量+1,表示释放当前资源,其他线程可以抢占了
[[KNetRequestManager share] getSomeData:^{
//网络请求成功,发送信号
dispatch_semaphore_signal(sema);
} errorBlock:^{
//网络请求失败,发送信号
dispatch_semaphore_signal(sema);
}];
//如果信号量为0,表示没有资源可用,便一直等待,不再往下执行
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
- (void)request3 {
//创建信号量,并设置为0,信号量本质是资源数,为0表示用完,需要等待
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
//模拟网络请求-异步
//每次网络请求成功或失败后,都让信号量+1,表示释放当前资源,其他线程可以抢占了
[[KNetRequestManager share] getSomeData:^{
//网络请求成功,发送信号
dispatch_semaphore_signal(sema);
} errorBlock:^{
//网络请求失败,发送信号
dispatch_semaphore_signal(sema);
}];
//如果信号量为0,表示没有资源可用,便一直等待,不再往下执行
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
当然,我们也可以使用dispatch_group_enter和dispatch_group_leave来实现同样的功能:
- (void)creatGCDSinger {
//创建线程组
dispatch_group_t group = dispatch_group_create();
//创建一个并发队列
dispatch_queue_t queue = dispatch_queue_create("group.queue", DISPATCH_QUEUE_CONCURRENT);
//任务1
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
//请求1
[self request1WithGroup:group];
});
//任务2
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
//请求2
[self request2WithGroup:group];
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"-------所有网络请求已请求完成-------");
});
}
- (void)request1WithGroup:(dispatch_group_t)group {
//模拟网络请求-异步
[[KNetRequestManager share] getSomeData:^{
//网络请求成功,调用level
dispatch_group_leave(group);
} errorBlock:^{
//网络请求失败,调用level
dispatch_group_leave(group);
}];
}
- (void)request2WithGroup:(dispatch_group_t)group
//模拟网络请求-异步
[[KNetRequestManager share] getSomeData:^{
//网络请求成功,调用level
dispatch_group_leave(group);
} errorBlock:^{
//网络请求失败,调用level
dispatch_group_leave(group);
}];
}
5.2.3信号量控制网络请求顺序
- (void)creatGCDSinger {
//创建semp
dispatch_semaphore_t semp = dispatch_semaphore_create(1);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//任务1
dispatch_async(queue, ^{
//信号量-1
dispatch_semaphore_wait(semp, DISPATCH_TIME_FOREVER);
//模拟网络请求
//模拟网络请求-异步
//每次网络请求成功或失败后,都让信号量+1,表示释放当前资源,其他线程可以抢占了
[[KNetRequestManager share] getSomeData:^{
//网络请求成功,发送信号
dispatch_semaphore_signal(sema);
} errorBlock:^{
//网络请求失败,发送信号
dispatch_semaphore_signal(sema);
}];
});
//任务2
dispatch_async(queue, ^{
//信号量-1
dispatch_semaphore_wait(semp, DISPATCH_TIME_FOREVER);
//模拟网络请求
//模拟网络请求-异步
//每次网络请求成功或失败后,都让信号量+1,表示释放当前资源,其他线程可以抢占了
[[KNetRequestManager share] getSomeData:^{
//网络请求成功,发送信号
dispatch_semaphore_signal(sema);
} errorBlock:^{
//网络请求失败,发送信号
dispatch_semaphore_signal(sema);
}];
});
}
6、队列的挂起和开启
在GCD框架中还提供了暂停与开始任务队列的方法,使用下面的函数可以将队列或队列组暂时挂起和开启:
//挂起队列或队列组
void dispatch_suspend(dispatch_object_t object);
//开启队列或队列组
void dispatch_resume(dispatch_object_t object);
注意:在暂停队列时,队列中正在执行的任务并不会中断,未开启的任务会被挂起。
7、数据存储的线程安全问题-多度单写
在进行多线程编程时,或许总会遇到这一类问题:数据的竞争与线程的安全。这些问题如果通过程序手动来控制,则难度将会非常大。CGD同样为我们简单地解决了这样的问题。
首先,如果只是在读取数据,而不对数据做任何修改,那么我们并不需要处理安全问题,可以让多个任务同时进行读取。可是如果要对数据进行写操作,那么在同一时间,我们就必须只能有一个任务在写,CGD中有一个方法帮我们完美地解决了这个问题,示例如下:
- (void)creatCGDReadAndWriter {
//创建一个队列
dispatch_queue_t queue = dispatch_queue_create("oneQueue", DISPATCH_QUEUE_CONCURRENT);
//多个任务同时执行读操作
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"read1:%d",i);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"read2:%d",i);
}
});
//执行写操作
/*
下面这个函数在加入队列时不会执行,会等待已经开始的异步执行全部完成后再执行,并且在执行时会阻塞其他任务
当执行完成后,其他任务重新进入异步执行
*/
dispatch_barrier_async(queue, ^{
for (int i = 0; i < 5; i ++) {
NSLog(@"writer:%d",i);
}
});
//绩效执行异步操作
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"read3:%d",i);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"read4:%d",i);
}
});
}
打印信息:
image.png
从打印信息可以看出读操作是异步进行的,写操作是等待当前任务结束后阻塞任务队列独立进行的,当写操作结束后队列恢复异步执行读操作,这正是我们需要的效果。