为什么dispatch_sync在自定义并发队列死锁上

时间:2012-12-19 23:20:09

标签: objective-c ios grand-central-dispatch

在自定义并发dispatch_queue上使用dispatch_sync时,我的应用程序中出现间歇性死锁。我正在使用与Mike Ash's blog中描述的方法类似的东西来支持并发读访问,但NSMutableDictionary上的线程安全突变充当当前活动网络RPC请求的缓存。我的项目使用ARC。

我使用以下命令创建队列:

dispatch_queue_t activeRequestsQueue = dispatch_queue_create("my.queue.name",
                                                DISPATCH_QUEUE_CONCURRENT);

和带有

的可变字典
NSMutableDictionary *activeRequests = [[NSMutable dictionary alloc] init];

我从队列中读取元素:

- (id)activeRequestForRpc: (RpcRequest *)rpc
{
    assert(![NSThread isMainThread]);
    NSString * key = [rpc getKey];
    __block id obj = nil;
    dispatch_sync(activeRequestsQueue, ^{
        obj = [activeRequests objectForKey: key];
    });
    return obj;
}

我在缓存中添加和删除rpcs

- (void)addActiveRequest: (RpcRequest *)rpc
{
    NSString * key = [rpc getKey];
    dispatch_barrier_async(activeRequestsQueue, ^{
        [activeRequests setObject: rpc forKey: key];
    });
}

- (void)removeActiveRequest: (RpcRequest *)rpc
{
    NSString * key = [rpc getKey];
    dispatch_barrier_async(activeRequestsQueue, ^{
        [activeRequests removeObjectForKey:key];
    });
}

当我立即发出大量网络请求时,我看到了对activeRequestForRpc调用的死锁,这让我相信其中一个障碍块(添加或删除)没有完成执行。我总是从后台线程调用activeRequestForRpc,并且应用程序UI不会冻结,所以我不认为它必须阻止主线程,但我添加了断言语句以防万一。关于如何发生这种僵局的任何想法?

更新:添加调用这些方法的代码

我正在使用AFNetworking来发出网络请求,我有一个NSOperationQueue,我正在安排'检查缓存,也许可以从网络中获取资源'逻辑。我将调用CheckCacheAndFetchFromNetworkOp。在该操作中,我调用AFHTTPClient的自定义子类来发出RPC请求。

// this is called from inside an NSOperation executing on an NSOperationQueue.
- (void) enqueueOperation: (MY_AFHTTPRequestOperation *) op {
    NSError *error = nil;
    if ([self activeRequestForRpc:op.netRequest.rpcRequest]) {
        error = [NSError errorWithDomain:kHttpRpcErrorDomain code:HttpRpcErrorDuplicate userInfo:nil];
    }
    // set the error on the op and cancels it so dependent ops can continue.
    [op setHttpRpcError:error];

    // Maybe enqueue the op
    if (!error) {
        [self addActiveRequest:op.netRequest.rpcRequest];
        [self enqueueHTTPRequestOperation:op];
    }
}

MY_AFHTTRequestOperation由AFHTTPClient实例构建,在成功和失败完成块内部,我将[self removeActiveRequest:netRequest.rpcRequest];称为第一个操作。 AFNetworking将这些块作为默认行为在主线程上执行。

我已经看到死锁发生在必须持有队列锁的最后一个障碍块是添加块和删除块的位置。

当系统产生更多线程来支持我的NSOperationQueue中的CheckCacheAndFetchFromNetworkOp Ops时,activeRequestsQueue的优先级是否太低而无法进行调度?如果所有线程都被CheckCacheAndFetchFromNetworkOps阻塞以尝试从activeRequests Dictionary读取,并且activeRequestsQueue在无法执行的添加/删除障碍块上阻塞,则可能导致死锁。

更新

通过将NSOperationQueue设置为maxConcurrentOperation计数为1(或者除了默认的NSOperationQueueDefaultMaxConcurrentOperationCount之外的其他任何合理值)来解决此问题。

基本上,我带走的教训是你不应该在任何其他dispatch_queue_t或NSOperationQueue上使用NSOperationQueue和默认的最大操作数等待,因为它可能会占用来自其他队列的所有线程。

这就是发生的事情。

队列 - 将NSOperationQueue设置为默认NSDefaultMaxOperationCount,以便系统确定要运行的并发操作数。

op - 在queue1上运行并在读取后在AFNetworking队列上调度网络请求,以确保RPC不在activeRequest集中。

以下是流程:

系统确定它可以支持10个并发线程(实际上它更像80)。

立即安排10个操作。系统允许10个操作在其10个线程上同时运行。所有10个操作都调用hasActiveRequestForRPC,它调度activeRequestQueue上的同步块并阻塞10个线程。 activeRequestQueue想要运行它的读取块,但没有任何可用的线程。此时我们已经陷入僵局。

更常见的是,我会看到9操作(1-9)被调度,其中一个,op1,在第10个线程上快速运行hasActiveRequestForRPC并调度addActiveRequest barrer块。然后另一个操作将在第10个线程上调度,op2-10将调度并等待hasActiveRequestForRPC。然后op1的预定addRpc块将不会运行,因为op10占用了最后一个可用线程,而所有其他hasActiveRequestForRpc块将等待屏障块执行。当op1试图在另一个也无法访问任何线程的操作队列上安排缓存操作时,op1最终会阻塞。

我假设阻塞hasActiveRequestForRPC正在等待barrer块执行,但关键是activeRequestQueue正在等待任何线程可用性。

1 个答案:

答案 0 :(得分:3)

编辑:结果问题是正在调用enqueueOperation:的NSOperationQueue正在使用所有可用线程,因为它们都在等待(通过dispatch_sync)在activeRequestsQueue上发生的事情。减少此队列上的maxConcurrentOperations解决了这个问题(请参阅注释),虽然这不是一个很好的解决方案,因为它会假设核心数量等。更好的解决方案是使用dispatch_async而不是{{ 1}},虽然这会使代码更复杂。

我之前的建议:

  • 当你已经在activeRequestsQueue上时,你正在调用dispatch_sync(并且你的断言由于某些原因没有触发,就像你在发布中运行一样。)

  • dispatch_sync(activeRequestsQueue, ...)导致请求被解除分配,dealloc正在等待调用[activeRequests removeObjectForKey:key];的内容,这会导致死锁。