关于信号量
信号量:就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。
其实,这有点类似锁机制了,只不过信号量都是系统帮助我们处理了,我们只需要在执行线程之前,设定一个信号量值,并且在使用时,加上信号量处理方法就行了。
信号量为0则阻塞线程,大于0则不会阻塞。因此我们可以通过改变信号量的值,来控制是否阻塞线程,从而达到线程同步。
在GCD中有三个函数是semaphore的操作,分别是:
dispatch_semaphore_create 创建一个semaphore
dispatch_semaphore_signal 发送一个信号
dispatch_semaphore_wait 等待信号
简单的介绍一下这三个函数,第一个函数有一个整形的参数,我们可以理解为信号的总量,dispatch_semaphore_signal是发送一个信号,自然会让信号总量加1,dispatch_semaphore_wait等待信号,当信号总量少于0的时候就会一直等待,否则就可以正常的执行,并让信号总量-1,根据这样的原理,我们便可以快速的创建一个并发控制来同步任务和有限资源访问控制。
实际开发中,我们通常会遇到如下问题:
一、某界面存在多个请求,希望所有请求均结束才进行某操作。
对于这一问题的解决方案很容易想到通过线程组进行实现,代码如下:
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求1
[self request_A];
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求2
[self request_B];
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求3
[self request_C];
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任务均完成,刷新界面");
});
这里我们书写一个网络请求通用方法,假设同时请求某新闻列表的3页数据,每页均为一个独立的网络请求。使用我们最常用的AFNet请求,方法如下(真实开发中可能为banner数据请求、主体网络请求、广告网络请求等):
- (void)request_A {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];;
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSDictionary *parameter = @{@"token":@"63104AB32427EBF89B957BBD1A5C5C11",
@"page":@"1",
@"upTime":@"desc"};
[manager POST:URL parameters:parameter progress:^(NSProgress * _Nonnull uploadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSDictionary * dict = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableContainers error:nil];
for (NSDictionary *rowsDict in dict[@"rows"]) {
NSLog(@"A___%@",rowsDict[@"title"]);
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
}
request_B、request_C分别为请求第二页与第三页数据,这里不重复书写。为了显示更加明显,在请求中打印了对应新闻的标题内容。
任务均完成,刷新界面
C___搞笑,不是认真的
C___摄影 | 街拍
C___生活小窍门
C___传统中国
C___想吃的美食系列
B___时间的见证者
B___没事 来吐槽吧……
B___触动心灵的摄影
B___摄影 | 黑白印记
B___每日插画推荐
A___左爱情,右面包
A___潮我看
A___世界各国的人们怎么过情人节
A___一点创意点亮生活
A___摄影 | 随手拍
运行后马上接收到了线程组完成的提示,之后数据才依次请求下来,很明显三个单纯的AFNet请求已经不能满足我们的需求了。线程组完成时并没有在我们希望的时候给予通知。在真实开发中会造成的问题为多个请求均加载完成,但界面已在未得到数据前提前刷新导致界面空白。
因此对于这种问题需要另辟蹊径,这里我们就要借助GCD中的信号量dispatch_semaphore进行实现,即营造线程同步情况。
dispatch_semaphore信号量为基于计数器的一种多线程同步机制。用于解决在多个线程访问共有资源时候,会因为多线程的特性而引发数据出错的问题。
如果semaphore计数大于等于1,计数-1,返回,程序继续运行。如果计数为0,则等待。
dispatch_semaphore_signal(semaphore)为计数+1操作。dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER)为设置等待时间,这里设置的等待时间是一直等待。
我们将网络请求通用方法进行修改如下:
- (void)request_A {
//创建信号量并设置计数默认为0
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];;
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSDictionary *parameter = @{@"token":@"63104AB32427EBF89B957BBD1A5C5C11",
@"page":@"1",
@"upTime":@"desc"};
[manager POST:URL parameters:parameter progress:^(NSProgress * _Nonnull uploadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
//计数+1操作
dispatch_semaphore_signal(sema);
NSDictionary * dict = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableContainers error:nil];
for (NSDictionary *rowsDict in dict[@"rows"]) {
NSLog(@"A___%@",rowsDict[@"title"]);
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
////计数+1操作
dispatch_semaphore_signal(sema);
}];
//若计数为0则一直等待
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
为方便阅读,伪代码如下:
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[网络请求:{
成功:dispatch_semaphore_signal(sema);
失败:dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
这时我们再运行程序,打印如下:
C___聆听的耳朵
C___家居 | 你的家里还缺点什么?
C___logo设计
C___时装
C___搞笑,不是认真的
A___左爱情,右面包
A___潮我看
A___世界各国的人们怎么过情人节
A___一点创意点亮生活
A___摄影 | 随手拍
B___时间的见证者
B___没事 来吐槽吧……
B___触动心灵的摄影
B___摄影 | 黑白印记
B___每日插画推荐
任务均完成,刷新界面
运行打印可见,通过信号量dispatch_semaphore完美的解决了此问题,并且网络请求仍为异步,不会堵塞当前主线程。
二、某界面存在多个请求,希望请求依次执行。
对于这个问题通常会通过线程依赖进行解决,因使用GCD设置线程依赖比较繁琐,这里通过NSOperationQueue进行实现,这里采用比较经典的例子,三个任务分别为下载图片,打水印和上传图片,三个任务需异步执行但需要顺序性。代码如下,下载图片、打水印、上传图片仍模拟为分别请求新闻列表3页数据。
//1.任务一:下载图片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
[self request_A];
}];
//2.任务二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
[self request_B];
}];
//3.任务三:上传图片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
[self request_C];
}];
//4.设置依赖
[operation2 addDependency:operation1]; //任务二依赖任务一
[operation3 addDependency:operation2]; //任务三依赖任务二
//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
首先我们使用未添加信号量dispatch_semaphore时运行,打印如下:
B___时间的见证者
B___没事 来吐槽吧……
B___触动心灵的摄影
B___摄影 | 黑白印记
B___每日插画推荐
A___潮我看
A___左爱情,右面包
A___世界各国的人们怎么过情人节
A___一点创意点亮生活
A___摄影 | 随手拍
C___盘
C___聆听的耳朵
C___家居 | 你的家里还缺点什么?
C___logo设计
C___时装
根据打印结果可见,若不对请求方法做处理,其运行结果并不是我们想要的,联系实际需求,A、B、C请求分别对应下载图片、打水印、上传图片,而此时运行顺序则为B->A->C,在未获得图片时即执行打水印操作明显是错误的。重复运行亦会出现不同结果,即请求不做处理,其结果不可控无法预测。线程依赖设置并未起到作用。
解决此问题的方法仍可通过信号量dispatch_semaphore进行解决。我们将请求方法替换为添加dispatch_semaphore限制的形式。即
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[网络请求:{
成功:dispatch_semaphore_signal(sema);
失败:dispatch_semaphore_signal(sema);
}]
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
再次重复运行,我们会发现每次运行结果均一致,A、B、C三任务异步顺序执行(A->B->C)
A___潮我看
A___左爱情,右面包
A___世界各国的人们怎么过情人节
A___一点创意点亮生活
A___摄影 | 随手拍
B___时间的见证者
B___没事 来吐槽吧……
B___触动心灵的摄影
B___摄影 | 黑白印记
B___每日插画推荐
C___盘
C___聆听的耳朵
C___家居 | 你的家里还缺点什么?
C___logo设计
C___时装
通过重复运行打印结果可证实确实实现了我们想要的效果。这样即解决了所提出的问题二。
这些就是我对GCD信号量的一点点理解,希望能够对大家有帮助。😁