一、控制最大并发数
对计算机了解的都会知道信号量的作用,当我们多个线程要访问同一个资源的时候,往往会设置一个信号量,当信号量大于0的时候,新的线程可以去操作这个资源,操作时信号量-1,操作完后信号量+1,当信号量等于0的时候,必须等待,所以通过控制信号量,我们可以控制能够同时进行的并发数。
信号量是一个整数,在创建的时候会有一个初始值,这个初始值往往代表我要控制的同时操作的并发数。在操作中,对信号量会有两种操作:信号通知与等待。信号通知时,信号量会+1,等待时,如果信号量大于0,则会将信号量-1,否则,会等待直到信号量大于0。什么时候会大于零呢?往往是在之前某个操作结束后,我们发出信号通知,让信号量+1。
说完概念,我们来看看GCD中的三个信号量操作:
dispatch_semaphore_create
:创建一个信号量(semaphore)dispatch_semaphore_signal
:信号通知,即让信号量+1dispatch_semaphore_wait
:等待,直到信号量大于0时,即可操作,同时将信号量-1
在 Objective-C 中,可以使用 dispatch_semaphore
来控制最大并发数量,限制同时执行的线程数量。通过设置初始值为最大并发数的 dispatch_semaphore
,可以实现限制并发执行的效果。
以下是一个示例代码,演示如何使用 dispatch_semaphore
控制最大并发数量:
objective-c
// 定义最大并发数
#define MAX_CONCURRENT_TASKS 3
dispatch_semaphore_t semaphore = dispatch_semaphore_create(MAX_CONCURRENT_TASKS); // 创建信号量,初始值为最大并发数
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 等待信号量
NSLog(@"Task %d started", i);
sleep(1); // 模拟任务执行
NSLog(@"Task %d finished", i);
dispatch_semaphore_signal(semaphore); // 释放信号量
});
}
在上面的示例中,定义了一个最大并发数为 3 的宏 MAX_CONCURRENT_TASKS
。然后创建了一个初始值为 MAX_CONCURRENT_TASKS
的 dispatch_semaphore
。在循环中,通过 dispatch_async
在全局队列中执行了 10 个任务,每个任务在开始时调用 dispatch_semaphore_wait
等待信号量,表示占用一个资源;在任务执行完成后调用 dispatch_semaphore_signal
释放信号量,表示释放资源。
上面代码表示我要操作10次,但是控制允许同时并发的操作最多只有3次,当并发量达到3后,信号量就减小到0了,这时候wait操作会起作用,DISPATCH_TIME_FOREVER表示会永远等待,一直等到信号量大于0,也就是有操作完成了,将信号量+1了,这时候才可以结束等待,进行操作,并且将信号量-1,这样新的任务又要等待。
通过这种方式,可以限制同时执行的线程数量为最大并发数,控制并发执行的效果。这种方法适用于需要限制并发数量的场景,例如网络请求、文件读写等操作。
二、控制多个请求结束后统一操作
假设我们一个页面需要同时进行多个请求,他们之间倒是不要求顺序关系,但是要求等他们都请求完毕了再进行界面刷新或者其他什么操作。
这个需求我们一般可以用GCD的group和notify来做到:
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求1
NSLog(@"Request_1");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求2
NSLog(@"Request_2");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//请求3
NSLog(@"Request_3");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//界面刷新
NSLog(@"任务均完成,刷新界面");
});
notify的作用就是在group中的其他操作全部完成后,再操作自己的内容,所以我们会看到上面三个内容都打印出来后,才打印界面刷新的内容。
但是当将上面三个操作改成真实的网络操作后,这个简单的做法会变得无效,为什么呢?因为网络请求需要时间,而线程的执行并不会等待请求完成后才真正算作完成,而是只负责将请求发出去,线程就认为自己的任务算完成了,当三个请求都发送出去,就会执行notify中的内容,但请求结果返回的时间是不一定的,也就导致界面都刷新了,请求才返回,这就是无效的。
要解决这个问题,我们就要用到上面说的信号量来操作了。
在每个请求开始之前,我们创建一个信号量,初始为0,在请求操作之后,我们设一个dispatch_semaphore_wait,在请求到结果之后,再将信号量+1,也即是dispatch_semaphore_signal。这样做的目的是保证在请求结果没有返回之前,一直让线程等待在那里,这样一个线程的任务一直在等待,就不会算作完成,notify的内容也就不会执行了,直到每个请求的结果都返回了,线程任务才能够结束,这时候notify也才能够执行。伪代码如下:
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[网络请求:{
成功:dispatch_semaphore_signal(sema);
失败:dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
三、控制多个请求顺序执行
有时候我们需要按照顺序执行多次请求,比如先请求到用户信息,然后根据用户信息中的内容去请求相关的数据,这在平常的代码中直接按照顺序往下写代码就可以了,但这里因为涉及到多线程之间的关系,就叫做线程依赖。
线程依赖用GCD做比较麻烦,建议用NSOperationQueue做,可以更加方便的设置任务之间的依赖。
// 1.任务一:获取用户信息
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
[self request_A];
}];
// 2.任务二:请求相关数据
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
[self request_B];
}];
// 3.设置依赖
[operation2 addDependency:operation1];// 任务二依赖任务一
// 4.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation2, operation1] waitUntilFinished:NO];
一般的多线程操作这样做是可以的,线程2会等待线程1完成后再执行。但是对于网络请求,问题又来了,同样,网络请求需要时间,线程发出请求后即认为任务完成了,并不会等待返回后的操作,这就失去了意义。
要解决这个问题,还是用信号量来控制,其实是一个道理,代码也是一样的,在一个任务操作中:
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[网络请求:{
成功:dispatch_semaphore_signal(sema);
失败:dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
还是去等待请求返回后,才让任务结束。而依赖关系则通过NSOperationQueue来实现。
其实归根结底,中心思想就是通过信号量,来控制线程任务什么时候算作结束,如果不用信号量,请求发出后即认为任务完成,而网络请求又要不同时间,所以会打乱顺序。因此用一个信号量来控制在单个线程操作内,必须等待请求返回,自己要执行的操作完成后,才将信号量+1,这时候一直处于等待的代码也得以执行通过,任务才算作完成。
通过这个方法,就可以解决由于网络请求耗时特性而带来的一些意想不到的多线程处理的问题。