Dlaczego dispatch_sync stosuje niestandardowe jednoczesne zakleszczanie kolejki

W aplikacji używam sporadycznego zakleszczenia podczas używania dispatch_sync na niestandardowej współbieżnej kolejce wysyłania. Używam czegoś podobnego do metody opisanej wBlog Mike'a Ash'a aby obsługiwać współbieżny dostęp do odczytu, ale mutacje wątkowe w NSMutableDictionary działające jako pamięć podręczna aktualnie aktywnych żądań RPC sieci. Mój projekt wykorzystuje ARC.

Tworzę kolejkę za pomocą:

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

i zmienny słownik z

NSMutableDictionary *activeRequests = [[NSMutable dictionary alloc] init];

Czytam elementy z kolejki w ten sposób:

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

Dodaję i usuwam rpcs z pamięci podręcznej

- (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];
    });
}

Widzę zakleszczenie w wywołaniu activeRequestForRpc, gdy wykonuję wiele żądań sieciowych naraz, co prowadzi mnie do przekonania, że ​​jeden z bloków blokujących (dodaj lub usuń) nie kończy wykonywania. Zawsze wywołuję activeRequestForRpc z wątku w tle, a interfejs użytkownika aplikacji nie zamarza, więc nie sądzę, aby musiał blokować główny wątek, ale na wszelki wypadek dodałem instrukcję assert. Jakieś pomysły na to, jak może dojść do tego impasu?

AKTUALIZACJA: dodawanie kodu, który wywołuje te metody

Używam AFNetworking do tworzenia żądań sieciowych i mam NSOperationQueue, które planuję logikę „sprawdzania pamięci podręcznej i może pobrać zasób z sieci”. Wywołam tę operację CheckCacheAndFetchFromNetworkOp. Wewnątrz tej operacji wykonuję wywołanie do mojej niestandardowej podklasy AFHTTPClient, aby wykonać żądanie 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];
    }
}

Operacja MY_AFHTTRequestOperation jest budowana przez instancję AFHTTPClient, a wewnątrz bloków zakończenia powodzenia i niepowodzenia wywoływam[self removeActiveRequest:netRequest.rpcRequest]; jako pierwsza akcja. Bloki te są wykonywane w głównym wątku przez AFNetworking jako zachowanie domyślne.

Widziałem zakleszczenie, w którym ostatni blokada, która musi blokować kolejkę, jest blokiem dodawania i blokiem usuwania.

Czy jest możliwe, że w miarę jak system tworzy więcej wątków do obsługi Opcji CheckCacheAndFetchFromNetworkOp w mojej NSOperationQueue, właściwość activeRequestsQueue byłaby zbyt niskim priorytetem, aby można ją było zaplanować? Może to spowodować zakleszczenie, jeśli wszystkie wątki zostały pobrane przez blokowanie CheckCacheAndFetchFromNetworkOps, aby spróbować odczytać ze słownika activeRequests, a activeRequestsQueue blokowało blokadę dodawania / usuwania, która nie mogła zostać wykonana.

AKTUALIZACJA

Naprawiono problem polegający na ustawieniu NSOperationQueue na liczbę maxConcurrentOperation 1 (lub naprawdę wszystko rozsądne, inne niż domyślny NSOperationQueueDefaultMaxConcurrentOperationCount).

Zasadniczo lekcja, którą zabrałem, polega na tym, że nie powinieneś mieć NSOperationQueue z domyślną liczbą operacji max oczekiwanie na dowolną inną dispatch_queue_t lub NSOperationQueue, ponieważ potencjalnie mogłaby zrzucić wszystkie wątki z tych innych kolejek.

Tak właśnie się działo.

kolejka - NSOperationQueue ustawiono na domyślną NSDefaultMaxOperationCount, która pozwala systemowi określić, ile współbieżnych operacji ma zostać uruchomionych.

op - uruchamia się w kolejce 1 i planuje żądanie sieciowe w kolejce AFNetworking po przeczytaniu, aby upewnić się, że RPC nie znajduje się w zestawie activeRequest.

Oto przepływ:

System określa, że ​​może obsługiwać 10 równoczesnych wątków (w rzeczywistości było to więcej niż 80).

10 operacji zostaje zaplanowanych jednocześnie. System pozwala na jednoczesne uruchomienie 10 operacji na 10 wątkach. Wszystkie 10 operacji wywołuje funkcję hasActiveRequestForRPC, która planuje blok synchronizacji w obiekcie activeRequestQueue i blokuje 10 wątków. ActiveRequestQueue chce uruchomić blok odczytu, ale nie ma żadnych dostępnych wątków. W tym momencie mamy już impas.

Częściej widziałem coś takiego, jak zaplanowano 9 operacji (1-9), jeden z nich, op1, szybko uruchamia hasActiveRequestForRPC w 10 wątku i planuje blok bariera addActiveRequest. Następnie kolejna operacja zostanie zaplanowana na 10 wątku, a op2-10 zaplanuje i poczeka na hasActiveRequestForRPC. Następnie zaplanowany blok addRpc op1 nie będzie działał, ponieważ op10 zajął ostatni dostępny wątek, a wszystkie inne bloki hasActiveRequestForRpc czekały na wykonanie bloku bariery. op1 skończy się blokowaniem później, gdy spróbuje zaplanować operację pamięci podręcznej na innej kolejce operacji, która również nie może uzyskać dostępu do żadnych wątków.

Zakładałem, że blokowanie hasActiveRequestForRPC czeka na wykonanie bloku barrer, ale kluczem jest oczekiwanie na activeRequestQueuekażdy dostępność wątku.

questionAnswers(1)

yourAnswerToTheQuestion