前言
在多线程和GCD文章中,我们总结了多线程的意义以及实际开发中的案例,其中死锁、线程安全等等都是多线程绕不过的问题,需要我们小心处理。今天我们继续总结下实际开发中还用到了哪些与GCD相关的应用以及需要注意的地方。
1、dispatch_group调度组
几个常用api如下:
dispatch_group_create:创建调度组dispatch_group_async:异步提交任务进调度组,需要注意的是使用这种方式进组可以省略enter和leave,下面会举例说明。dispatch_group_enter:手动管理调度组的进出dispatch_group_leavedispatch_group_notifydispatch_group_waitdispatch_group_wait:在调度组完成时调用,或者调度组超时调用。(完成指的是enter和leave次数一样多)ispatch_group_notify:不管超不超时,只要任务组完成,会调用,不完成不会调用。
调度组使用案例
假如有5个任务1、2、3、4、5,任务3必须要2之后执行,任务5必须在前面4个任务完成之后再执行。
分析:任务3必须在任务2之后执行,那么任务2和3应该是串联任务,把2和3作为一个串联任务整体,那么这个任务整体可以和任务1、4并行执行,最后的任务5必须等前面几个任务完成之后才能执行,一个典型的调度组应用。
//串行队列 放2、3
dispatch_queue_t serialqueue=dispatch_queue_create("aa", DISPATCH_QUEUE_SERIAL);
//并发队列 放 1、4
dispatch_queue_t concurrentqueue=dispatch_queue_create("bb", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t mygroup=dispatch_group_create();
dispatch_group_async(mygroup, concurrentqueue, ^{
NSLog(@"任务1执行");
});
dispatch_group_async(mygroup, concurrentqueue, ^{
NSLog(@"任务4执行");
});
dispatch_group_async(mygroup, serialqueue, ^{
sleep(1);
NSLog(@"任务2执行");
});
//enter和leave组合等价于dispatch_group_async
dispatch_group_enter(mygroup);
dispatch_async(serialqueue, ^{
NSLog(@"任务3执行");
dispatch_group_leave(mygroup);
});
dispatch_group_wait(mygroup, DISPATCH_TIME_FOREVER);
dispatch_group_notify(mygroup,dispatch_get_main_queue(), ^{
NSLog(@"任务5执行");
});
输出:
任务1执行
任务4执行
任务2执行
任务3执行
任务5执行
分析:
- 任务1、任务4和任务2、3整体上是没有先后顺序的,但是3肯定是在2之后执行。
- 注意
enter和leave组合等价于dispatch_group_async,上面执行任务3的时候就是用的enter和leave手动管理的调度组。
2、信号量
常用api如下:
dispatch_semaphore_create:创建一个信号量,初始值必须大于等于0,否则会失败dispatch_semaphore_signal:发送信号,会使信号量值value+1dispatch_semaphore_wait:等待信号量,会使信号量值value-1。当value值为0时,调用该函数就会阻塞所在线程,会持续等待,要么超时要么等待过程中value值大于0继续执行,看到阻塞就要千万注意了别阻塞主线程哦~
通过实例理解一下:
dispatch_semaphore_t sempt=dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//value=0时信号量-1,会阻塞线程,直到value>0才继续执行
dispatch_semaphore_wait(sempt, DISPATCH_TIME_FOREVER);
sleep(2);
NSLog(@"任务1执行");
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
NSLog(@"任务2执行");
//信号量+1,此时value=0,dispatch_semaphore_wait接收到信号不会阻塞线程
dispatch_semaphore_signal(sempt);
});
输出:
任务2执行
任务1执行
分析:并发队列中,执行dispatch_semaphore_wait时信号量值会减1,由于初始信号量的值为0,此时信号量值等于-1,会阻塞线程执行。执行完任务2,dispatch_semaphore_signal会使信号量值加1,此时信号量值等于0,dispatch_semaphore_wait会收到信号量值变化的广播,大于等于0就继续向下执行任务1。
如果再添加一个任务3呢?
dispatch_semaphore_t sempt=dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//value=0时信号量-1,此时信号量值为-1
dispatch_semaphore_wait(sempt, DISPATCH_TIME_FOREVER);
sleep(4);
NSLog(@"任务1执行");
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
NSLog(@"任务2执行");
//信号量+1,此时value=0,dispatch_semaphore_wait接收到信号不会阻塞线程
dispatch_semaphore_signal(sempt);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//此时信号量值为-1
dispatch_semaphore_wait(sempt, DISPATCH_TIME_FOREVER);
NSLog(@"任务3执行");
dispatch_semaphore_signal(sempt);
})
输出:
任务2执行
任务1执行
分析:为什么任务3不会执行?初始信号量值为0,所以任务1和任务3都是阻塞状态,当任务2执行完成后信号量+1,任务1的dispatch_semaphore_wait会收到通知继续向下执行,由于任务1没有调用dispatch_semaphore_signal使信号量+1,所以任务3还是阻塞状态不会执行。任务1的dispatch_semaphore_wait等待信号量在任务3的前面,如果在任务1的dispatch_semaphore_wait前面加一句sleep(1),那么会先执行2,再执行3,最后执行1,感兴趣的可以试试。
信号量的使用案例-线程安全
比如有100个线程同时往同一个数组中添加数据,如何保证内存安全呢?
NSMutableArray* arrar=[[NSMutableArray alloc]init];
dispatch_semaphore_t sempt=dispatch_semaphore_create(1);
for(int i=0;i<100;i++){
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_wait(sempt, DISPATCH_TIME_FOREVER);
NSLog(@"开始添加");
[arrar addObject:@(1)];
NSLog(@"结束添加");
dispatch_semaphore_signal(sempt);
});
}
输出:
开始添加
结束添加
开始添加
结束添加
开始添加
结束添加
开始添加
结束添加
开始添加
结束添加
开始添加
结束添加
.....
分析:通过信号量可以用来控制线程并发访问的最大数量,当我们设置信号量的初时值为1时,就可以实现线程同步。
3、信号源
Dispatch Source信号源,区别于dispatch_semaphore,信号源是用来监听事件的对象,这点和NSRunloop很类似,但是Dispatch Source是针对一个队列的,至于队列使用哪个线程执行这个task,则取决于队列的线程池状态,而NSRunloop则是完全与指定线程绑定的。
常用api如下:
dispatch_source_create:创建信号源,有4个参数 第一个参数type:信号源的类型,分为有11个类型,常用的是DISPATCH_SOURCE_TYPE_DATA_ADD和DISPATCH_SOURCE_TYPE_TIMER
DISPATCH_SOURCE_TYPE_DATA_ADD:属于自定义事件,可以通过dispatch_source_get_data函数获取事件变量数据,在我们自定义的方法中可以调用dispatch_source_merge_data函数向Dispatch Source设置数据。DISPATCH_SOURCE_TYPE_DATA_OR:属于自定义事件,用法同上面的类型一样。DISPATCH_SOURCE_TYPE_MACH_SEND:Mach端口发送事件。DISPATCH_SOURCE_TYPE_MACH_RECV:Mach端口接收事件。DISPATCH_SOURCE_TYPE_PROC:与进程相关的事件。DISPATCH_SOURCE_TYPE_READ:读文件事件。DISPATCH_SOURCE_TYPE_WRITE:写文件事件。DISPATCH_SOURCE_TYPE_VNODE:文件属性更改事件。DISPATCH_SOURCE_TYPE_SIGNAL:接收信号事件。DISPATCH_SOURCE_TYPE_TIMER:定时器事件。DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:内存压力事件。
dispatch_suspend(queue):挂起信号源(队列也可以调用该函数挂起)dispatch_resume(source) :开启信号源(队列也可以调用该函数开启),信号源刚创建时是挂起状态,需要调用该函数开启。dispatch_source_merge_data:向信号源发送传值事件,不可以传递0值或负值,否则事件不会被触发dispatch_source_set_event_handler:设置响应发送的事件。- dispatch_source_get_data:得到信号源发送的值。
- dispatch_source_cancel:取消信号源,即不再监听,区别于挂起状态。
- dispatch_source_get_handle:得到dispatch_source_create的第二个参数
先用一个简单的网络请求数据完成后派送信号源更新UI的例子
dispatch_source_t source=dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_source_set_event_handler(source, ^{
//获取信号源中派发的值
dispatch_source_get_data(source);
//更新ui操作....
});
//开启监听
dispatch_resume(source);
//网络请求等等完成后通知信号源,并发送数据
dispatch_source_merge_data(source, 1);
定时器信号源案例
__block int timeout = 60;
//创建队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//创建timer
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置1s触发一次,0s的误差精度
dispatch_source_set_timer(_timer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0); //每秒执行
//事件的处理
dispatch_source_set_event_handler(_timer, ^{
if(timeout<=0){ //倒计时结束,关闭
//取消dispatch源
dispatch_source_cancel(_timer);
}
else{
timeout--;
dispatch_async(dispatch_get_main_queue(), ^{
//更新主界面的操作
NSLog(@"%d秒后可重新获取验证码",timeout);
});
}
});
//开启信号源监听
dispatch_resume(_timer);
分析:dispatch_source_set_timer,该函数的作用就是给监听事件类型为DISPATCH_SOURCE_TYPE_TIMER的Dispatch Source设置相关属性,该函数有四个参数:
source:该参数为目标Dispatch Source,类型为dispatch_source_t.start:该参数为定时器的起始时间,类型为dispatch_time_t。interval:该参数为定时器的间隔时间,类型为UInt64,间隔时间的单位是纳秒。leeway:该参数为间隔时间的精度,类型为UInt64,时间单位也是纳秒。
进度条信号源案例
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
@property (nonatomic, strong) dispatch_source_t source;
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, assign) NSUInteger totalComplete;
@property (nonatomic) BOOL isRunning;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//初始进度条
self.totalComplete = 0;
//串行队列
self.queue = dispatch_queue_create("ahua.com", NULL);
//自定义信号源
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
//监听信号源派送的事件
dispatch_source_set_event_handler(self.source, ^{
//信号源每次发送的值都是1,进度条++1
NSUInteger value = dispatch_source_get_data(self.source);
self.totalComplete += value;
NSLog(@"进度: %.2f",self.totalComplete/100.0);
self.progressView.progress = self.totalComplete/100.0;
});
self.isRunning = YES;
//开启监听
dispatch_resume(self.source);
}
//点击按钮开启或者暂停信号源
- (IBAction)didClickStartOrPauseAction:(id)sender {
if (self.isRunning) {
//挂起信号源和队列
dispatch_suspend(self.source);
dispatch_suspend(self.queue);
NSLog(@"已经暂停");
self.isRunning = NO;
[sender setTitle:@"暂停中.." forState:UIControlStateNormal];
}else{
dispatch_resume(self.source);
dispatch_resume(self.queue);
NSLog(@"已经执行了");
self.isRunning = YES;
[sender setTitle:@"执行中.." forState:UIControlStateNormal];
}
}
//点击屏幕向信号源派送事件,模拟触发进度条更新
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
for (int i= 0; i<100; i++) {
//串行队列每一秒派送一次事件
dispatch_async(self.queue, ^{
if (!self.isRunning) {
NSLog(@"已经暂停");
return;
}
sleep(1);
dispatch_source_merge_data(self.source, 1);
});
}
}