iOS开发基础83-信号量与网络请求同步:多请求并发与依赖的完整解决方案
GCD 信号量与网络请求同步:多请求并发与依赖的完整解决方案
在实际开发中,我们经常遇到这样的场景:一个页面有多个网络请求,需要等所有请求都完成后再刷新 UI;或者几个请求有先后依赖关系,必须 A 完成后再执行 B。本文从这两个经典问题出发,深入解析 dispatch_group、dispatch_semaphore、NSOperationQueue 依赖的正确用法,常见陷阱(异步请求在 group 中立即返回、依赖对异步任务无效),并给出更优雅的解决方案和注意事项。
一、问题背景:异步网络请求的完成时机
一句话原理
网络请求是异步的——发起请求后函数立即返回,数据要等一段时间后才通过回调返回。所以"请求函数执行完毕"不等于"请求真正完成",这是所有多请求同步问题的根源。
两个经典问题
| 问题 | 场景 | 关键词 |
|---|---|---|
| 问题一 | 页面有 A、B、C 三个请求,希望全部完成后再刷新 UI | 并发等待、group |
| 问题二 | A、B、C 三个请求有依赖,必须依次执行(A→B→C) | 串行依赖、operation |
这两个问题的核心难点都是:如何判断异步网络请求真正完成了。用普通的
dispatch_group_async或NSOperation依赖是不够的,因为它们只能保证"block 执行完毕",而不是"网络回调触发"。
二、问题一:多个请求全部完成后再执行操作
1. 常见错误写法:dispatch_group_async
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
[self request_A]; // 异步请求,函数立即返回
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
[self request_B];
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
[self request_C];
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"任务均完成,刷新界面");
});
为什么不对?
dispatch_group_async 只会等 block 执行完毕。而 request_A 是异步网络请求,block 里调用 [self request_A] 后函数立即返回,block 就结束了——但网络请求还在后台跑,数据还没回来。
所以 dispatch_group_notify 会在三个 block 都执行完后立即触发,而此时三个网络请求都还没返回数据。结果就是:界面先刷新了,数据后到,界面空白。
这正是原文运行结果显示的问题:"任务均完成,刷新界面"先打印,然后才是 A/B/C 的数据。
2. 正确写法一:dispatch_group_enter / dispatch_group_leave(推荐)
一句话原理
dispatch_group_enter 手动给 group 计数 +1,dispatch_group_leave 手动 -1;只有当计数归零时,dispatch_group_notify 才会触发。这样我们就可以在网络回调里调用 leave,精确控制"请求真正完成"的时机。
完整代码
- (void)loadAllData {
dispatch_group_t group = dispatch_group_create();
// 请求 A
dispatch_group_enter(group); // 进入 group,计数 +1
[self request_AWithCompletion:^{
dispatch_group_leave(group); // 请求完成,离开 group,计数 -1
}];
// 请求 B
dispatch_group_enter(group);
[self request_BWithCompletion:^{
dispatch_group_leave(group);
}];
// 请求 C
dispatch_group_enter(group);
[self request_CWithCompletion:^{
dispatch_group_leave(group);
}];
// 所有请求都完成后(计数归零),回到主线程刷新 UI
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"A、B、C 全部完成,刷新界面");
[self.tableView reloadData];
});
}
网络请求方法需要改造为带 completion 回调
- (void)request_AWithCompletion:(void (^)(void))completion {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
NSDictionary *parameter = @{@"page": @"1"};
[manager POST:URL parameters:parameter progress:nil
success:^(NSURLSessionDataTask *task, id responseObject) {
// 处理数据...
NSLog(@"A 请求完成");
if (completion) completion(); // 成功回调
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(@"A 请求失败: %@", error.localizedDescription);
if (completion) completion(); // 失败也要回调,否则 group 计数不归零
}];
}
关键注意事项
| 注意点 | 说明 |
|---|---|
| enter 和 leave 必须成对出现 | 每个 enter 必须对应一个 leave,否则 group 永远不会完成 |
| 成功和失败都要调用 leave | 网络请求失败时也必须调用 completion()(即 leave),否则计数不归零,notify 永远不触发 |
| 非阻塞 | dispatch_group_enter/leave 不会阻塞当前线程,请求是并发执行的,性能好 |
| 不需要信号量 | 这是比信号量方案更优雅的方式,不会阻塞线程 |
3. 正确写法二:dispatch_semaphore 信号量
一句话原理
信号量是一个计数器:wait 时如果计数为 0 就阻塞等待,signal 时计数 +1 唤醒等待的线程。用信号量可以把异步网络请求"伪装"成同步的——发起请求后阻塞等待,回调里 signal 唤醒。
完整代码
- (void)loadAllDataWithSemaphore {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 请求 A
dispatch_group_async(group, queue, ^{
[self request_A_Sync]; // 内部用信号量等待请求完成
});
// 请求 B
dispatch_group_async(group, queue, ^{
[self request_B_Sync];
});
// 请求 C
dispatch_group_async(group, queue, ^{
[self request_C_Sync];
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"全部完成,刷新界面");
});
}
// 用信号量把异步请求变成同步等待
- (void)request_A_Sync {
// 创建信号量,初始计数为 0
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
[manager POST:URL parameters:@{@"page": @"1"} progress:nil
success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(@"A 完成");
dispatch_semaphore_signal(sema); // 计数 +1,唤醒等待的线程
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(@"A 失败");
dispatch_semaphore_signal(sema); // 失败也要 signal,否则永久等待
}];
// 计数为 0 时阻塞当前线程,直到 signal 被调用
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
信号量方案的注意事项
| 注意点 | 说明 |
|---|---|
| 必须在后台线程调用 | 如果在主线程调用 dispatch_semaphore_wait,而网络回调也在主线程(AFN 默认主线程回调),会死锁!必须把请求放到后台队列 |
| 成功失败都要 signal | 失败回调里也必须调用 signal,否则线程永久等待 |
| 建议设置超时 | 用 DISPATCH_TIME_FOREVER 有风险,如果网络一直不返回,线程一直阻塞。可以设置超时时间 |
| 会阻塞线程 | 每个请求占用一个线程等待,请求数量多时线程开销大 |
设置超时时间
// 等待 10 秒,超时后继续执行(不会永久卡住)
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC);
long result = dispatch_semaphore_wait(sema, timeout);
if (result == 0) {
NSLog(@"请求正常完成");
} else {
NSLog(@"请求超时");
}
4. 两种方案对比
| 对比项 | dispatch_group_enter/leave | dispatch_semaphore |
|---|---|---|
| 阻塞线程 | ❌ 不阻塞,非阻塞式 | ✅ 会阻塞当前线程 |
| 线程开销 | 小(不需要额外线程等待) | 大(每个请求占一个线程等待) |
| 死锁风险 | 无 | 有(主线程等待 + 主线程回调 = 死锁) |
| 代码复杂度 | 低(只需 enter/leave 配对) | 中(需要管理信号量创建和等待) |
| 并发性能 | 好(真正并发) | 好(也能并发,但阻塞线程) |
| 推荐程度 | ⭐⭐⭐⭐⭐ 推荐 | ⭐⭐⭐ 特殊场景用 |
结论:问题一(多请求并发等待全部完成)优先用
dispatch_group_enter/leave,更优雅、不阻塞线程、无死锁风险。信号量方案适合问题二(有依赖的串行执行)或需要把异步变同步的特殊场景。
三、问题二:多个请求依次执行(有依赖关系)
1. 常见错误写法:NSOperationQueue 依赖
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[self request_A]; // 异步请求,立即返回
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[self request_B];
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
[self request_C];
}];
[op2 addDependency:op1]; // op2 依赖 op1
[op3 addDependency:op2];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[op1, op2, op3] waitUntilFinished:NO];
为什么不对?
NSBlockOperation 的 block 执行完毕就认为 operation 完成了。而 request_A 是异步的,block 调用后立即返回,operation 就标记为完成了——但网络请求还没回来。
所以依赖关系变成了:op1 的 block 执行完 → op2 开始 → op2 的 block 执行完 → op3 开始。但 A 的数据还没回来,B 就已经开始了,依赖完全失效。
运行结果就是 A/B/C 的顺序不可控,可能是 B→A→C,每次运行都不一样。
2. 正确写法一:信号量 + 串行队列(最简单)
一句话原理
在串行队列中,用信号量把每个异步请求变成同步等待,这样前一个请求真正完成后,后一个才会开始。
完整代码
- (void)loadDataSerial {
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serial", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
[self request_A_Sync]; // 内部信号量等待
});
dispatch_async(serialQueue, ^{
[self request_B_Sync]; // A 完成后才会执行到这里
});
dispatch_async(serialQueue, ^{
[self request_C_Sync]; // B 完成后才会执行到这里
// 全部完成,回主线程刷新
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"A→B→C 全部依次完成");
[self.tableView reloadData];
});
});
}
request_A_Sync就是上一节用信号量封装的同步等待方法。串行队列保证任务按顺序执行,信号量保证每个任务等网络请求真正完成后才结束。
3. 正确写法二:NSOperation 子类封装异步任务(最规范)
一句话原理
NSBlockOperation 只能封装同步任务,要封装异步任务,需要自定义 NSOperation 子类,重写 start 方法,手动管理 isExecuting 和 isFinished 状态,在网络回调里才标记为完成。
完整代码
// NetworkOperation.h
@interface NetworkOperation : NSOperation
@property (nonatomic, copy) NSString *url;
@property (nonatomic, copy) NSDictionary *parameters;
@property (nonatomic, strong) id responseData;
@property (nonatomic, strong) NSError *error;
- (instancetype)initWithURL:(NSString *)url parameters:(NSDictionary *)parameters;
@end
// NetworkOperation.m
#import "NetworkOperation.h"
#import <AFNetworking/AFNetworking.h>
@interface NetworkOperation ()
@property (nonatomic, assign, getter=isExecuting) BOOL executing;
@property (nonatomic, assign, getter=isFinished) BOOL finished;
@end
@implementation NetworkOperation
@synthesize executing = _executing;
@synthesize finished = _finished;
- (instancetype)initWithURL:(NSString *)url parameters:(NSDictionary *)parameters {
self = [super init];
if (self) {
_url = url;
_parameters = parameters;
// 关键:异步 operation 必须是并发的
self.executing = NO;
self.finished = NO;
}
return self;
}
// 标记为异步 operation(并发)
- (BOOL)isAsynchronous {
return YES;
}
- (void)start {
// 检查是否被取消
if ([self isCancelled]) {
[self willChangeValueForKey:@"isFinished"];
self.finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
[self willChangeValueForKey:@"isExecuting"];
self.executing = YES;
[self didChangeValueForKey:@"isExecuting"];
// 发起异步网络请求
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
[manager POST:self.url parameters:self.parameters progress:nil
success:^(NSURLSessionDataTask *task, id responseObject) {
if (![self isCancelled]) {
self.responseData = responseObject;
}
[self completeOperation];
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
self.error = error;
[self completeOperation];
}];
}
// 标记 operation 完成
- (void)completeOperation {
[self willChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];
self.executing = NO;
self.finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
- (void)cancel {
[super cancel];
if (self.isExecuting) {
[self completeOperation];
}
}
@end
使用自定义 Operation + 依赖
- (void)loadDataWithOperation {
NetworkOperation *opA = [[NetworkOperation alloc] initWithURL:URL parameters:@{@"page": @"1"}];
NetworkOperation *opB = [[NetworkOperation alloc] initWithURL:URL parameters:@{@"page": @"2"}];
NetworkOperation *opC = [[NetworkOperation alloc] initWithURL:URL parameters:@{@"page": @"3"}];
// 设置依赖:B 依赖 A,C 依赖 B
[opB addDependency:opA];
[opC addDependency:opB];
// 监听完成
[opC setCompletionBlock:^{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"A→B→C 全部依次完成");
NSLog(@"A 的数据: %@", opA.responseData);
NSLog(@"B 的数据: %@", opB.responseData);
NSLog(@"C 的数据: %@", opC.responseData);
});
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[opA, opB, opC] waitUntilFinished:NO];
}
自定义 Operation 的关键点
| 关键点 | 说明 |
|---|---|
isAsynchronous 返回 YES |
标记这是异步 operation,系统不会在 start 方法返回时就认为完成 |
手动管理 isExecuting / isFinished |
必须手动触发 KVO 通知(willChangeValueForKey / didChangeValueForKey),队列才能感知状态变化 |
在网络回调里调用 completeOperation |
只有网络真正完成(成功或失败)后才标记 finished |
| 处理取消 | start 时检查 isCancelled,被取消时直接标记 finished |
| 线程安全 | executing 和 finished 属性的读写需要考虑线程安全(简单场景下在主线程或串行队列操作即可) |
这种方式最规范,适合大型项目中大量有依赖关系的异步任务管理。但代码量较大,简单场景用信号量 + 串行队列更省事。
4. 正确写法三:回调嵌套(最简单但不优雅)
- (void)loadDataNested {
[self request_AWithCompletion:^{
NSLog(@"A 完成");
[self request_BWithCompletion:^{
NSLog(@"B 完成");
[self request_CWithCompletion:^{
NSLog(@"C 完成,全部结束");
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}];
}];
}];
}
优点:简单直接,不需要 GCD 知识。
缺点:回调地狱(Callback Hell),嵌套层级深了之后可读性极差,难以维护。
只适合 2~3 层的简单依赖,层级多了建议用其他方案。
5. 三种方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 信号量 + 串行队列 | 简单、代码量少 | 阻塞线程、有死锁风险 | 简单的串行依赖 |
| 自定义 NSOperation | 规范、可取消、可控制并发数 | 代码量大、需要理解 KVO 状态管理 | 大型项目、复杂依赖关系 |
| 回调嵌套 | 最简单、无额外知识 | 回调地狱、难维护 | 2~3 层简单依赖 |
四、dispatch_semaphore 深入解析
1. 一句话原理
信号量是一个计数器,用来控制同时访问某个资源的线程数量。计数 > 0 时可以通过,计数 = 0 时等待。
2. 三个核心函数
| 函数 | 作用 | 类比 |
|---|---|---|
dispatch_semaphore_create(value) |
创建信号量,设置初始计数值 | 餐厅有 value 个座位 |
dispatch_semaphore_wait(sema, timeout) |
计数 -1,如果计数为 0 则阻塞等待 | 顾客进门,占一个座位;没座位就排队等 |
dispatch_semaphore_signal(sema) |
计数 +1,唤醒一个等待的线程 | 顾客离开,空出一个座位,叫下一位顾客 |
3. 常见用途
| 用途 | 说明 |
|---|---|
| 异步变同步 | 本文的核心场景:网络请求发起后 wait,回调里 signal |
| 控制最大并发数 | 创建信号量初始值为 5,最多同时 5 个任务执行 |
| 线程安全 | 保护共享资源,同一时间只有一个线程访问(类似 NSLock) |
| 任务等待 | 等待某个异步操作完成后再继续 |
4. 控制最大并发数示例
// 最多同时 3 个请求并发
dispatch_semaphore_t sema = dispatch_semaphore_create(3);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 10; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); // 占一个名额
NSLog(@"开始请求 %ld", (long)i);
// 模拟网络请求
[self requestWithCompletion:^{
NSLog(@"完成请求 %ld", (long)i);
dispatch_semaphore_signal(sema); // 释放名额
}];
});
}
10 个请求,但最多同时 3 个在执行,其他的在排队等待。这在图片批量下载、接口限流等场景很有用。
五、更现代的方案:ReactiveObjC / Promise 链式调用
对于中大型项目,手动管理 group、信号量、依赖比较繁琐,推荐用响应式或 Promise 风格的库。
1. ReactiveObjC 链式请求
// 问题一:多个请求并发,全部完成后处理
RACSignal *signalA = [self requestSignal_A];
RACSignal *signalB = [self requestSignal_B];
RACSignal *signalC = [self requestSignal_C];
// combineLatest: 合并多个信号,所有信号都发送 next 后才触发
[[RACSignal combineLatest:@[signalA, signalB, signalC]]
subscribeNext:^(RACTuple *tuple) {
NSLog(@"A、B、C 全部完成");
NSLog(@"A: %@, B: %@, C: %@", tuple.first, tuple.second, tuple.third);
}];
// 问题二:有依赖的串行请求(then: 上一个完成后才执行下一个)
[[[[self requestSignal_A]
then:^RACSignal *{
NSLog(@"A 完成,开始 B");
return [self requestSignal_B];
}]
then:^RACSignal *{
NSLog(@"B 完成,开始 C");
return [self requestSignal_C];
}]
subscribeNext:^(id value) {
NSLog(@"C 完成,全部结束");
}];
ReactiveObjC 的
then、combineLatest、zip等操作符天然支持异步任务的编排,比手动管理 GCD 优雅得多。
2. PromiseKit(Swift 更常用)
// 并发等待
firstly {
when(fulfilled: requestA(), requestB(), requestC())
}.done { a, b, c in
print("全部完成")
}.catch { error in
print("出错: \(error)")
}
// 串行依赖
firstly {
requestA()
}.then { a in
requestB()
}.then { b in
requestC()
}.done { c in
print("全部完成")
}.catch { error in
print("出错: \(error)")
}
六、Swift 版本对照
1. DispatchGroup(对应 dispatch_group)
func loadAllData() {
let group = DispatchGroup()
group.enter()
requestA {
group.leave()
}
group.enter()
requestB {
group.leave()
}
group.notify(queue: .main) {
print("全部完成,刷新 UI")
}
}
2. DispatchSemaphore(对应 dispatch_semaphore)
func requestASync() {
let sema = DispatchSemaphore(value: 0)
AF.request(url).responseData { response in
switch response.result {
case .success(let data):
print("A 完成")
case .failure(let error):
print("A 失败: \(error)")
}
sema.signal()
}
sema.wait() // 阻塞等待
}
3. OperationQueue 依赖(对应 NSOperationQueue)
let opA = BlockOperation {
self.requestASync() // 同步等待的请求
}
let opB = BlockOperation {
self.requestBSync()
}
let opC = BlockOperation {
self.requestCSync()
}
opB.addDependency(opA)
opC.addDependency(opB)
let queue = OperationQueue()
queue.addOperations([opA, opB, opC], waitUntilFinished: false)
Swift 中 GCD 的语法更简洁:
DispatchGroup、DispatchSemaphore、DispatchQueue,用法和 OC 一致。
七、常见问题与坑
问题 1:在主线程调用信号量 wait 会死锁吗?
答案:很可能会死锁!
AFNetworking 的 success/failure 回调默认在主线程。如果你在主线程调用 dispatch_semaphore_wait,主线程被阻塞了,而网络回调也要回到主线程执行,回调永远执行不了,signal 永远不会被调用,主线程永远等待——死锁。
解决方法:
- 把信号量等待的代码放到后台队列执行(
dispatch_async(globalQueue, ^{ ... }))。 - 或者把 AFNetworking 的回调队列设为后台队列:
manager.completionQueue = dispatch_get_global_queue(0, 0);。 - 或者用
dispatch_group_enter/leave方案(非阻塞,无死锁风险)。
问题 2:网络请求失败了,group 永远不完成怎么办?
答案:必须在成功和失败两个回调里都调用 leave(或 signal)。
很多人只在 success 里调用,失败了就不调用,导致 group 计数不归零,notify 永远不触发。正确做法是无论成功失败,只要请求结束了(不管结果如何),都要通知 group。
// 正确:成功失败都调用 completion
success:^(...) {
if (completion) completion();
}
failure:^(...) {
if (completion) completion(); // 失败也要调用!
}
问题 3:DISPATCH_TIME_FOREVER 有什么风险?
答案:如果网络一直不返回(比如服务器挂了、网络断了但没触发失败回调),线程会永久阻塞。
解决方法:设置超时时间:
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC);
long result = dispatch_semaphore_wait(sema, timeout);
if (result != 0) {
NSLog(@"请求超时,继续执行后续逻辑");
}
或者给网络请求本身设置超时(
manager.requestSerializer.timeoutInterval = 15;),双保险。
问题 4:dispatch_group_enter/leave 和 dispatch_group_async 有什么区别?
| 对比 | dispatch_group_async | dispatch_group_enter/leave |
|---|---|---|
| 适用任务 | 同步任务(block 执行完即完成) | 异步任务(需要手动控制完成时机) |
| 完成时机 | block 执行完毕 | 手动调用 leave |
| 能否用于网络请求 | ❌ 不能(block 立即返回) | ✅ 可以(在回调里 leave) |
记住:异步任务用 enter/leave,同步任务用 async。
问题 5:NSOperation 依赖对异步任务无效,怎么解决?
答案:两种方式:
- 用自定义
NSOperation子类,重写start,手动管理isExecuting/isFinished,在异步回调里才标记 finished(本文第三节方案二)。 - 用信号量把异步任务变成同步的,再用
NSBlockOperation包装(本文第三节方案一)。
问题 6:多个请求有依赖,但中间需要传递数据怎么办?
比如 B 请求需要 A 请求返回的 token,C 请求需要 B 返回的 ID。
用回调嵌套最直接:
[self request_AWithCompletion:^(NSString *token) {
[self request_BWithToken:token completion:^(NSString *userId) {
[self request_CWithUserId:userId completion:^{
NSLog(@"全部完成");
}];
}];
}];
或者用 ReactiveObjC 的 then 传递数据:
[[[[self requestSignal_A]
then:^RACSignal *(NSString *token) {
return [self requestSignal_BWithToken:token];
}]
then:^RACSignal *(NSString *userId) {
return [self requestSignal_CWithUserId:userId];
}]
subscribeNext:^(id value) {
NSLog(@"全部完成");
}];
问题 7:信号量和 NSLock 有什么区别?
| 对比 | dispatch_semaphore | NSLock |
|---|---|---|
| 本质 | 计数器(可以控制 N 个线程同时访问) | 互斥锁(只能 1 个线程访问) |
| 初始值 | 可以是任意正整数 | 固定为 1(互斥) |
| 超时等待 | 支持 dispatch_semaphore_wait(timeout) |
不支持(只能 lock 阻塞或 tryLock 尝试) |
| 性能 | 高(GCD 底层实现) | 中等(pthread_mutex 封装) |
| 用途 | 控制并发数、异步变同步、线程同步 | 保护临界区、线程互斥 |
信号量初始值为 1 时,效果等同于 NSLock。但信号量更灵活,可以控制多个线程同时访问。
八、总结
- 核心问题:异步网络请求的"函数执行完毕"不等于"请求真正完成",这是所有多请求同步问题的根源。
- 问题一(多请求并发,全部完成后处理):
- ❌ 错误:
dispatch_group_async包裹异步请求(block 立即返回,group 提前完成)。 - ✅ 推荐:
dispatch_group_enter/leave,在网络回调里调用leave,非阻塞、无死锁。 - ✅ 备选:
dispatch_semaphore信号量,把异步变同步,但要在后台线程调用,注意死锁。
- ❌ 错误:
- 问题二(多请求有依赖,依次执行):
- ❌ 错误:
NSBlockOperation+ 依赖(block 立即返回,依赖失效)。 - ✅ 简单方案:信号量 + 串行队列。
- ✅ 规范方案:自定义
NSOperation子类,手动管理isExecuting/isFinished。 - ✅ 最简单:回调嵌套(层级多了会回调地狱)。
- ❌ 错误:
- 信号量核心:计数器,
wait时计数为 0 则阻塞,signal时计数 +1 唤醒。注意必须成对、必须在后台线程、必须设置超时。 - 更优雅的方案:ReactiveObjC(
then/combineLatest)或 PromiseKit,天然支持异步任务编排。 - 面试高频考点:
- 为什么
dispatch_group_async对异步网络请求无效? dispatch_group_enter/leave的正确用法?- 信号量的原理和使用注意事项?
- 如何让
NSOperation支持异步任务? - 主线程调用信号量 wait 为什么会死锁?
- 为什么

浙公网安备 33010602011771号