Obsługa wątków

Model wątków Bindera został zaprojektowany tak, aby ułatwiać lokalne wywołania funkcji, nawet jeśli są one kierowane do zdalnego procesu. W szczególności każdy proces hostujący węzeł musi mieć pulę co najmniej 1 wątku powiązanego, aby obsługiwać transakcje z węzłami hostowanymi w tym procesie.

Transakcje synchroniczne i asynchroniczne

Binder obsługuje transakcje synchroniczne i asynchroniczne. W sekcjach poniżej znajdziesz wyjaśnienie, jak wykonuje się poszczególne typy transakcji.

Transakcje synchroniczne

Transakcje synchroniczne są blokowane do momentu wykonania ich w węźle i otrzymania przez wywołującego odpowiedzi na tę transakcję. Na ilustracji poniżej widać, jak przebiega transakcja synchroniczna:

Transakcja synchroniczna.

Rysunek 1. Transakcja synchroniczna.

Aby wykonać transakcję synchroniczną, usługa Binder wykonuje te czynności:

  1. Wątki w docelowej puli wątków (T2 i T3) wywołują sterownik jądra, aby czekać na przychodzące zadania.
  2. Jądro otrzymuje nową transakcję i budzi wątek (T2) w procesie docelowym, aby obsłużyć transakcję.
  3. Wątek wywołujący (T1) blokuje się i czeka na odpowiedź.
  4. Proces docelowy wykonuje transakcję i zwraca odpowiedź.
  5. Wątek w procesie docelowym (T2) wywołuje sterownik jądra, aby czekać na nowe zadania.

Transakcje asynchroniczne

Transakcje asynchroniczne nie blokują się do momentu ukończenia. Wątek wywołujący odblokowuje się, gdy tylko transakcja zostanie przekazana do jądra. Na poniższym rysunku pokazano, jak jest wykonywana transakcja asynchroniczna:

Transakcja asynchroniczna.

Rysunek 2. Transakcja asynchroniczna.

  1. Wątki w docelowej puli wątków (T2 i T3) wywołują sterownik jądra, aby czekać na przychodzące zadania.
  2. Jądro otrzymuje nową transakcję i budzi wątek (T2) w procesie docelowym, aby obsłużyć transakcję.
  3. Wątek wywołujący (T1) kontynuuje wykonywanie.
  4. Proces docelowy wykonuje transakcję i zwraca odpowiedź.
  5. Wątek w procesie docelowym (T2) wywołuje sterownik jądra, aby czekać na nowe zadania.

Określanie funkcji synchronicznej lub asynchronicznej

Funkcje oznaczone w pliku AIDL jako oneway są asynchroniczne. Na przykład:

oneway void someCall();

Jeśli funkcja nie jest oznaczona jako oneway, jest funkcją synchroniczną, nawet jeśli zwraca wartość void.

Serializacja transakcji asynchronicznych

Binder serializuje transakcje asynchroniczne z dowolnego pojedynczego węzła. Na poniższym rysunku pokazano, jak Binder serializuje transakcje asynchroniczne:

Serializacja transakcji asynchronicznych.

Rysunek 3. Serializacja transakcji asynchronicznych.

  1. Wątki w docelowej puli wątków (B1 i B2) wywołują sterownik jądra, aby czekać na przychodzące zadania.
  2. Do jądra wysyłane są 2 transakcje (T1 i T2) w tym samym węźle (N1).
  3. Jądro otrzymuje nowe transakcje i ponieważ pochodzą one z tego samego węzła (N1), serializuje je.
  4. Do jądra wysyłana jest kolejna transakcja na innym węźle (N2).
  5. Jądro otrzymuje trzecią transakcję i wybudza wątek (B2) w procesie docelowym, aby obsłużyć transakcję.
  6. Procesy docelowe wykonują każdą transakcję i zwracają odpowiedź.

Zagnieżdżone transakcje

Transakcje synchroniczne mogą być zagnieżdżone. Wątek obsługujący transakcję może wydać nową transakcję. Zagnieżdżona transakcja może być skierowana do innego procesu lub do tego samego procesu, z którego pochodzi bieżąca transakcja. Działanie to przypomina lokalne wywołania funkcji. Załóżmy na przykład, że masz funkcję z zagnieżdżonymi funkcjami:

def outer_function(x):
    def inner_function(y):
        def inner_inner_function(z):

Jeśli są to wywołania lokalne, są one wykonywane w tym samym wątku. Jeśli wywołujący funkcję inner_function jest jednocześnie procesem hostującym węzeł, który implementuje funkcję inner_inner_function, wywołanie funkcji inner_inner_function jest wykonywane w tym samym wątku.

Na poniższym rysunku pokazano, jak moduł wiążący obsługuje zagnieżdżone transakcje:

Zagnieżdżone transakcje.

Rysunek 4. Zagnieżdżone transakcje.

  1. Wątek A1 prosi o uruchomienie foo().
  2. W ramach tego żądania wątek B1 uruchamia bar(), które wątek A uruchamia w tym samym wątku A1.

Na poniższym rysunku przedstawiono wykonanie wątku, jeśli węzeł implementujący interfejs bar() znajduje się w innym procesie:

Zagnieżdżone transakcje w różnych procesach.

Rysunek 5. Zagnieżdżone transakcje w różnych procesach.

  1. Wątek A1 prosi o uruchomienie foo().
  2. W ramach tego żądania wątek B1 uruchamia bar(), które działa w innym wątku C1.

Na poniższym rysunku widać, jak wątek ponownie wykorzystuje ten sam proces w dowolnym miejscu w łańcuchu transakcji:

Zagnieżdżone transakcje ponownie wykorzystujące wątek.

Rysunek 6. Zagnieżdżone transakcje ponownie wykorzystujące wątek.

  1. Proces A wywołuje proces B.
  2. Proces B wywołuje proces C.
  3. Proces C wywołuje proces A, a jądro ponownie wykorzystuje wątek A1 w procesie A, który jest częścią łańcucha transakcji.

W przypadku transakcji asynchronicznych zagnieżdżanie nie odgrywa żadnej roli. Klient nie czeka na wynik transakcji asynchronicznej, więc nie ma zagnieżdżania. Jeśli program obsługi transakcji asynchronicznej wywołuje proces, który wydał tę transakcję asynchroniczną, transakcja może być obsługiwana na dowolnym wolnym wątku w tym procesie.

Unikanie zakleszczeń

Obraz poniżej przedstawia typowy zakleszczenie:

Typowy impas.

Rysunek 7. Typowy impas.

  1. Proces A przejmuje mutex MA i wykonuje wywołanie bindera (T1) do procesu B, który również próbuje przejąć mutex MB.
  2. Jednocześnie proces B pobiera mutex MB i wykonuje wywołanie Binder (T2) do procesu A, który próbuje pobrać mutex MA.

Jeśli te transakcje się pokrywają, każda z nich może zablokować mutex w swoim procesie, czekając, aż drugi proces zwolni mutex, co spowoduje zakleszczenie.

Aby uniknąć zakleszczeń podczas korzystania z bindera, nie przytrzymuj żadnej blokady podczas wykonywania wywołania bindera.

Reguły kolejności blokad i zakleszczenia

W jednym środowisku wykonawczym zakleszczenia często unika się za pomocą reguły kolejności blokowania. Jednak podczas wywoływania funkcji między procesami i bazami kodu, zwłaszcza w miarę aktualizowania kodu, nie można utrzymać i koordynować reguły kolejności.

Pojedynczy mutex i zakleszczenia

W przypadku transakcji zagnieżdżonych proces B może bezpośrednio wywołać ten sam wątek w procesie A, który zawiera mutex. Z tego powodu z powodu nieoczekiwanej rekurencji nadal może dojść do zakleszczenia przy użyciu jednego muteksu.

Wywołania synchroniczne i zakleszczenia

Asynchroniczne wywołania interfejsu nie blokują się do momentu zakończenia, ale należy też unikać blokowania w przypadku wywołań asynchronicznych. Jeśli masz blokadę, możesz napotkać problemy z blokowaniem, gdy połączenie jednokierunkowe zostanie przypadkowo zmienione na połączenie synchroniczne.

Pojedynczy wątek powiązania i zakleszczenia

Model transakcji Binder umożliwia ponowne wejście, więc nawet jeśli proces ma jeden wątek Binder, nadal potrzebujesz blokowania. Załóżmy na przykład, że iterujesz po liście w procesie A z jednym wątkiem. W przypadku każdego elementu na liście wykonujesz transakcję wychodzącą. Jeśli implementacja wywoływanej funkcji powoduje nową transakcję bindera do węzła hostowanego w procesie A, transakcja ta jest obsługiwana na tym samym wątku, który iterował listę. Jeśli implementacja tej transakcji modyfikuje tę samą listę, możesz napotkać problemy podczas dalszego iterowania po liście.

Konfigurowanie rozmiaru puli wątków

Jeśli usługa ma wielu klientów, dodanie większej liczby wątków do puli wątków może zmniejszyć rywalizację i umożliwić równoległą obsługę większej liczby wywołań. Po prawidłowym rozwiązaniu problemu z jednoczesnością możesz dodać więcej wątków. Problem, który może być spowodowany dodaniem większej liczby wątków, niż jest potrzebna w przypadku mniejszego obciążenia.

Wątki są tworzone na żądanie do momentu osiągnięcia skonfigurowanej maksymalnej liczby. Po utworzeniu wątek powiązania pozostaje aktywny do momentu zakończenia procesu, w którym jest hostowany.

Biblioteka libbinder ma domyślnie 15 wątków. Aby zmienić tę wartość, użyj:setThreadPoolMaxThreadCount

using ::android::ProcessState;
ProcessState::self()->setThreadPoolMaxThreadCount(size_t maxThreads);