提供带有目标回调队列的异步和同步 API

Offering both asynchronous and synchronous API with a target callback queue

我正在写一个网络API。由于对 NSURLSession 的底层调用始终是异步的,因此我默认提供异步 API:

- (void) callBackendServerWithCompletion: (dispatch_block_t) completion;

提供此 API 的同步版本也非常方便,例如简化 Xcode 游乐场中的代码测试。同步调用按照异步调用来写:

- (void) callBackendSynchronously
{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self callBackendServerWithCompletion:^{
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

这很好用。

现在我想添加一个额外的便利功能,一个默认的调度队列来调用完成块。此回调队列默认为 UI 队列,因此此 API 的消费者不必一直 dispatch_async(dispatch_get_main_queue(), ^{…}):

// This:
[webservice callBackendServerWithCompletion:^{
    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateUI];
    });
}];

// Would be replaced with this:
[webservice callBackendServerWithCompletion:^{
    // Guaranteed to run on the main queue
    [self updateUI];
}];

这很容易做到,但现在我在主队列上调用同步方法时遇到了死锁:

  1. -callBackendSynchronously 调用 -callBackendServerWithCompletion 并等待信号量。
  2. 异步方法处理网络请求并在主队列上调度回调。
  3. 由于主队列已经在等待信号量,代码死锁。

提供所有三个功能的简单方法是什么,即。同步和异步 API 方法和默认回调队列?

添加 callBackendServerWithCompletion 的私有重载版本,接受调度队列。在 callBackendSynchronously 中,使用自定义后台队列调用这个新的重载方法。

最后,在您原来的callBackendServerWithCompletion 方法中调用重载版本,将默认队列作为参数传递。

一个简单的解决方法是不将回调队列添加为 属性,而是作为异步调用的参数:

/// Guaranteed to call the completion on the main queue
- (void) callBackendServerWithCompletion: (dispatch_block_t) completion;
/// Pick your own callback queue
- (void) callBackendServerWithTargetQueue: (dispatch_queue_t) callbackQueue completion: (dispatch_block_t) completion;

然后同步方法可以为回调指定一个全局队列,打破死锁,因为信号量是从另一个线程发出信号的:

- (void) callBackendSynchronously
{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self callBackendServerWithCallbackQueue:dispatch_get_global_queue(0, 0) completion:^{
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

我还不确定是否有一些缺点。

像这样实现同步 API 会导致 QOS 和重要性继承出现问题。我强烈建议您改变您的范式,尽可能避免使用信号量。假设你有一个序列化你的操作的操作队列,你可以这样做:

-(void)doItAsyncWithCompletionHandler:(nullable void (^)(NSError * _Nullable error)completionHandler
{
    [self doItAsyncWithCompletionQueue:nil completionHandler:completionHandler];
}

-(void)doItAsyncWithCompletionQueue:(nullable dispatch_queue_t)completionQueue
                  completionHandler:(nullable void (^)(NSError * _Nullable error)completionHandler
{
    if (!completionQueue) {
        completionQueue = dispatch_get_global_queue(qos_class_self(), 0);
    }

    completionHandler = completionHandler.copy;

    dispatch_async(self.operationQueue, ^{
        NSError *error;
        BOOL success = [self _onOperationQueueDoItWithError:&error];
        NSAssert((success && !error) || (!success && error), @"API Contract violation in -_onOperationQueueDoItWithError:");

        if (completionHandler) {
            dispatch_async(completionQueue, ^{
                completionHandler(error);
            });
        }
    });
}

-(BOOL)doItSyncWithError:(NSError * __autoreleasing _Nullable * _Nullable)error
{
    __block BOOL success;

    dispatch_sync(self.operationQueue, ^{
        success = [self _onOperationQueueDoItWithError:error];
    });

    return success;
}

-(BOOL)_onOperationQueueDoItWithError:(NSError * __autoreleasing _Nullable * _Nullable)error
{
    dispatch_assert_queue(self.operationQueue);

    ...
}