12

在自定义并发 dispatch_queue 上使用 dispatch_sync 时,我的应用程序出现间歇性死锁。我正在使用类似于Mike Ash 的博客中描述的方法来支持并发读取访问,但 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;
}

我从缓存中添加和删除 RPC

- (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 不会冻结,所以我认为它不必阻塞主线程,但我添加了 assert 语句以防万一。关于这种僵局如何发生的任何想法?

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

我正在使用 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 字典中读取,并且 activeRequestsQueue 阻塞在无法执行的添加/删除屏障块上,这可能会导致死锁。

更新

通过将 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 块。然后另一个 op 将被安排在第 10 个线程上,并且 op2-10 将安排并等待 hasActiveRequestForRPC。然后 op1 的调度 addRpc 块将不会运行,因为 op10 占用了最后一个可用线程,并且所有其他 hasActiveRequestForRpc 块将等待屏障块执行。当 op1 试图在另一个也无法访问任何线程的操作队列上安排缓存操作时,它最终会阻塞。

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

4

1 回答 1

3

编辑:原来问题是正在调用的 NSOperationQueueenqueueOperation:正在使用所有可用线程,所以因为它们都在等待(通过 dispatch_sync)在activeRequestsQueue. 减少这个队列上的 maxConcurrentOperations 解决了这个问题(见评论),虽然这并不是一个很好的解决方案,因为它对核心数量等进行了假设。更好的解决方案是使用dispatch_async而不是dispatch_sync,尽管这会使代码更复杂。

我之前的建议:

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

  • [activeRequests removeObjectForKey:key];导致请求被释放,而 dealloc 正在等待调用 的东西activeRequestForRpc:,这将导致死锁。

于 2012-12-20T00:46:07.963 回答