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 依赖对异步任务无效,怎么解决?

答案:两种方式:

  1. 用自定义 NSOperation 子类,重写 start,手动管理 isExecuting/isFinished,在异步回调里才标记 finished(本文第三节方案二)。
  2. 用信号量把异步任务变成同步的,再用 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 为什么会死锁?

posted @ 2017-04-28 17:06  Mr.陳  阅读(660)  评论(0)    收藏  举报