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

Ted*_*son 12 objective-c grand-central-dispatch ios

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

我创建队列:

dispatch_queue_t activeRequestsQueue = dispatch_queue_create("my.queue.name",
                                                DISPATCH_QUEUE_CONCURRENT);
Run Code Online (Sandbox Code Playgroud)

和可变字典

NSMutableDictionary *activeRequests = [[NSMutable dictionary alloc] init];
Run Code Online (Sandbox Code Playgroud)

我从队列中读取元素,如下所示:

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

我在缓存中添加和删除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];
    });
}
Run Code Online (Sandbox Code Playgroud)

当我立刻发出大量网络请求时,我看到了对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];
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

UPDATE

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

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

这就是发生的事情.

queue - 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等待任何线程可用性.

Jes*_*sak 3

编辑:事实证明,问题在于正在调用的 NSOperationQueueenqueueOperation:正在使用所有可用线程,因此因为它们都在等待(通过dispatch_sync)发生在activeRequestsQueue. 减少此队列上的 maxConcurrentOperations 解决了问题(请参阅注释),尽管这并不是一个很好的解决方案,因为它对核心数量等做出了假设。更好的解决方案是使用而不是dispatch_asyncdispatch_sync尽管这将使代码更复杂。

我之前的建议:

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

  • [activeRequests removeObjectForKey:key];导致请求被释放,并且 dealloc 正在等待调用 的内容activeRequestForRpc:,这会导致死锁。