Подтвердить что ты не робот

Почему dispatch_sync в пользовательской одновременной блокировке очереди

Я вижу прерывистый тупик в своем приложении при использовании dispatch_sync в пользовательской параллельной диспетчерской_экспо. Я использую что-то похожее на метод, описанный в Блог Майка Эша для поддержки одновременного доступа к чтению, но потокобезопасные мутации 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 из фонового потока, и пользовательский интерфейс приложения не замерзает, поэтому я не думаю, что он должен блокировать основной поток, но я добавил инструкцию assert на всякий случай. Любые идеи о том, как может произойти этот тупик?

ОБНОВЛЕНИЕ: добавление кода, вызывающего эти методы

Я использую AFNetworking для создания сетевых запросов, и у меня есть NSOperationQueue, что я планирую логику "check cache и, возможно, выборку из сети". Я назову это в CheckCacheAndFetchFromNetworkOp. Внутри этого op я вызываю свой собственный подкласс 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 в качестве поведения по умолчанию.

Я видел тупик, когда последний барьерный блок, который должен удерживать блокировку в очереди, является блоком добавления и блоком удаления.

Возможно ли, что по мере того, как система генерирует больше потоков для поддержки CheckCacheAndFetchFromNetworkOp Ops в моем NSOperationQueue, activeRequestsQueue будет слишком низким приоритетом для планирования? Это может вызвать тупик, если все потоки были сделаны блокировкой CheckCacheAndFetchFromNetworkOps, чтобы попытаться прочитать из словаря activeRequests, а activeRequestsQueue блокирует блок блокировки добавления/удаления, который не смог выполнить.

UPDATE

Исправлена ​​ошибка, устанавливая NSOperationQueue значение maxConcurrentOperation, равное 1 (или действительно ничего разумного, кроме стандартного NSOperationQueueDefaultMaxConcurrentOperationCount).

В основном урок, который я убрал, заключается в том, что у вас не должно быть NSOperationQueue с ожидаемым подсчетом максимального количества операций по умолчанию для любых других dispatch_queue_t или NSOperationQueue, поскольку он может потенциально зависеть от всех потоков из этих других очередей.

Это то, что происходило.

queue - NSOperationQueue установлен по умолчанию NSDefaultMaxOperationCount, который позволяет системе определить, сколько одновременных операций запускать.

op - запускается в очереди1 и после чтения считывает сетевой запрос в очереди AFNetworking, чтобы убедиться, что RPC не находится в наборе activeRequest.

Вот поток:

Система определяет, что она может поддерживать 10 параллельных потоков (на самом деле это больше похоже на 80).

10 ops получают запланированное время. Система позволяет 10 операциям запускать одновременно 10 потоков. Все 10 вызовов ops hasActiveRequestForRPC, которые планируют блок синхронизации на activeRequestQueue и блокируют 10 потоков. ActiveRequestQueue хочет запустить блок чтения, но не имеет доступных потоков. На этом мы уже зашли в тупик.

Чаще всего я бы увидел что-то вроде 9 ops (1-9), которые запланированы, один из них, op1, быстро запускает hasActiveRequestForRPC на 10-м потоке и планирует блок barrer addActiveRequest. Затем другой op будет назначен на 10-й поток, а op2-10 будет планировать и ждать на hasActiveRequestForRPC. Тогда блок addRpc, запланированный op1, не будет запущен, так как op10 занял последний доступный поток, а все остальные блоки hasActiveRequestForRpc будут ждать выполнения барьерного блока. op1 завершит блокировку позже, когда попытается запланировать операцию кэширования в другой очереди операций, которая также не сможет получить доступ к каким-либо потокам.

Я предполагал, что блокировка hasActiveRequestForRPC ожидала выполнения блока баррера, но ключ был активнымRequestQueue, ожидающим наличия любой.

4b9b3361

Ответ 1

EDIT: Оказывается, проблема заключается в том, что NSOperationQueue, который вызывает enqueueOperation:, использует все доступные потоки, так что они все ждут (через dispatch_sync), чтобы что-то произошло на activeRequestsQueue. Уменьшение maxConcurrentOperations в этой очереди решило проблему (см. Комментарии), хотя это не очень хорошее решение, поскольку оно делает предположения о количестве ядер и т.д. Лучшим решением было бы использовать dispatch_async, а не dispatch_sync, хотя это сделает код более сложным.

Мои предыдущие предложения:

  • Вы вызываете dispatch_sync(activeRequestsQueue, ...), когда вы уже находитесь в activeRequestsQueue (и ваш assert не по какой-то причине не стреляет, например, вы работаете в версии.)

  • [activeRequests removeObjectForKey:key]; вызывает освобождение запроса, а dealloc ожидает чего-то, что вызывает activeRequestForRpc:, что может вызвать тупик.