Gérer les fils de discussion

Le modèle de threading de Binder est conçu pour faciliter les appels de fonction locaux, même si ces appels peuvent s'adresser à un processus distant. Plus précisément, tout processus hébergeant un nœud doit disposer d'un pool d'un ou plusieurs threads de binder pour gérer les transactions vers les nœuds hébergés dans ce processus.

Transactions synchrones et asynchrones

Binder est compatible avec les transactions synchrones et asynchrones. Les sections suivantes expliquent comment chaque type de transaction est exécuté.

Transactions synchrones

Les transactions synchrones sont bloquées jusqu'à ce qu'elles aient été exécutées sur le nœud et qu'une réponse pour cette transaction ait été reçue par l'appelant. La figure suivante montre comment une transaction synchrone est exécutée :

Transaction synchrone.

Figure 1 : Transaction synchrone.

Pour exécuter une transaction synchrone, le binder procède comme suit :

  1. Les threads des pools de threads cibles (T2 et T3) appellent le pilote du noyau pour attendre le travail entrant.
  2. Le noyau reçoit une nouvelle transaction et réveille un thread (T2) dans le processus cible pour gérer la transaction.
  3. Le thread appelant (T1) se bloque et attend une réponse.
  4. Le processus cible exécute la transaction et renvoie une réponse.
  5. Le thread du processus cible (T2) rappelle le pilote du noyau pour attendre de nouvelles tâches.

Transactions asynchrones

Les transactions asynchrones ne bloquent pas l'exécution jusqu'à leur achèvement. Le thread appelant est débloqué dès que la transaction a été transmise au noyau. La figure suivante montre comment une transaction asynchrone est exécutée :

Transaction asynchrone.

Figure 2. Transaction asynchrone.

  1. Les threads des pools de threads cibles (T2 et T3) appellent le pilote du noyau pour attendre le travail entrant.
  2. Le noyau reçoit une nouvelle transaction et réveille un thread (T2) dans le processus cible pour gérer la transaction.
  3. Le thread appelant (T1) poursuit son exécution.
  4. Le processus cible exécute la transaction et renvoie une réponse.
  5. Le thread du processus cible (T2) rappelle le pilote du noyau pour attendre de nouvelles tâches.

Identifier une fonction synchrone ou asynchrone

Les fonctions marquées comme oneway dans le fichier AIDL sont asynchrones. Exemple :

oneway void someCall();

Si une fonction n'est pas marquée comme oneway, il s'agit d'une fonction synchrone, même si elle renvoie void.

Sérialisation des transactions asynchrones

Binder sérialise les transactions asynchrones à partir de n'importe quel nœud. La figure suivante montre comment Binder sérialise les transactions asynchrones :

Sérialisation des transactions asynchrones.

Figure 3. Sérialisation des transactions asynchrones.

  1. Les threads du pool de threads cible (B1 et B2) appellent le pilote du noyau pour attendre le travail entrant.
  2. Deux transactions (T1 et T2) sur le même nœud (N1) sont envoyées au noyau.
  3. Le noyau reçoit de nouvelles transactions et, comme elles proviennent du même nœud (N1), il les sérialise.
  4. Une autre transaction sur un nœud différent (N2) est envoyée au noyau.
  5. Le noyau reçoit la troisième transaction et réveille un thread (B2) dans le processus cible pour gérer la transaction.
  6. Les processus cibles exécutent chaque transaction et renvoient une réponse.

Transactions imbriquées

Les transactions synchrones peuvent être imbriquées. Un thread qui gère une transaction peut émettre une nouvelle transaction. La transaction imbriquée peut être destinée à un autre processus ou au même processus que celui à partir duquel vous avez reçu la transaction actuelle. Ce comportement imite les appels de fonctions locales. Par exemple, supposons que vous ayez une fonction avec des fonctions imbriquées :

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

S'il s'agit d'appels locaux, ils sont exécutés sur le même thread. Plus précisément, si l'appelant de inner_function est également le processus hébergeant le nœud qui implémente inner_inner_function, l'appel à inner_inner_function est exécuté sur le même thread.

La figure suivante montre comment Binder gère les transactions imbriquées :

Transactions imbriquées.

Figure 4. Transactions imbriquées.

  1. Le thread A1 demande l'exécution de foo().
  2. Dans le cadre de cette requête, le thread B1 exécute bar(), que A exécute sur le même thread A1.

La figure suivante montre l'exécution des threads si le nœud qui implémente bar() se trouve dans un processus différent :

Transactions imbriquées dans différents processus.

Figure 5. Transactions imbriquées dans différents processus.

  1. Le thread A1 demande l'exécution de foo().
  2. Dans le cadre de cette requête, le thread B1 exécute bar(), qui s'exécute dans un autre thread C1.

La figure suivante montre comment le thread réutilise le même processus n'importe où dans la chaîne de transactions :

Transactions imbriquées réutilisant un thread.

Figure 6. Transactions imbriquées réutilisant un thread.

  1. Le processus A appelle le processus B.
  2. Le processus B appelle le processus C.
  3. Le processus C effectue ensuite un rappel dans le processus A, et le noyau réutilise le thread A1 du processus A qui fait partie de la chaîne de transactions.

Pour les transactions asynchrones, l'imbrication n'a pas d'importance. Le client n'attend pas le résultat d'une transaction asynchrone, il n'y a donc pas d'imbrication. Si le gestionnaire d'une transaction asynchrone effectue un appel dans le processus qui a émis cette transaction asynchrone, cette transaction peut être gérée sur n'importe quel thread libre de ce processus.

Éviter les blocages

L'image suivante montre un cas d'impasse courant :

Interblocage courant.

Figure 7. Interblocage courant.

  1. Le processus A prend le mutex MA et effectue un appel de liaison (T1) au processus B, qui tente également de prendre le mutex MB.
  2. Simultanément, le processus B prend le mutex MB et effectue un appel Binder (T2) au processus A, qui tente de prendre le mutex MA.

Si ces transactions se chevauchent, chaque transaction peut potentiellement prendre un mutex dans son processus en attendant que l'autre processus libère un mutex, ce qui entraîne un blocage.

Pour éviter les blocages lors de l'utilisation du binder, ne maintenez aucun verrou lors d'un appel binder.

Règles d'ordre de verrouillage et blocages

Dans un environnement d'exécution unique, l'impasse est souvent évitée grâce à une règle d'ordre de verrouillage. Toutefois, lorsqu'il s'agit d'effectuer des appels entre les processus et entre les bases de code, en particulier lorsque le code est mis à jour, il est impossible de maintenir et de coordonner une règle de classement.

Mutex unique et blocages

Avec les transactions imbriquées, le processus B peut rappeler directement le même thread dans le processus A qui détient un mutex. Par conséquent, en raison d'une récursion inattendue, il est toujours possible d'obtenir un blocage avec un seul mutex.

Appels synchrones et blocages

Bien que les appels Binder asynchrones ne bloquent pas l'exécution, vous devez également éviter de conserver un verrou pour les appels asynchrones. Si vous détenez un verrou, vous pouvez rencontrer des problèmes de verrouillage si un appel unidirectionnel est accidentellement transformé en appel synchrone.

Thread Binder unique et blocages

Le modèle de transaction de Binder permet la réentrance. Par conséquent, même si un processus ne comporte qu'un seul thread Binder, vous avez toujours besoin d'un verrouillage. Par exemple, supposons que vous itériez sur une liste dans un processus A à thread unique. Pour chaque élément de la liste, vous effectuez une transaction Binder sortante. Si l'implémentation de la fonction que vous appelez effectue une nouvelle transaction de binder vers un nœud hébergé dans le processus A, cette transaction est gérée sur le même thread qui itérait la liste. Si l'implémentation de cette transaction modifie la même liste, vous risquez de rencontrer des problèmes lorsque vous continuerez à itérer sur la liste ultérieurement.

Configurer la taille du pool de threads

Lorsqu'un service comporte plusieurs clients, l'ajout de threads au pool de threads peut réduire les conflits et traiter davantage d'appels en parallèle. Une fois que vous avez géré correctement la concurrence, vous pouvez ajouter d'autres threads. Problème qui peut être causé par l'ajout de threads supplémentaires, dont certains peuvent ne pas être utilisés lors de charges de travail calmes.

Les threads sont générés à la demande jusqu'à un maximum configuré. Une fois qu'un thread de binder a été généré, il reste actif jusqu'à la fin du processus qui l'héberge.

La bibliothèque libbinder est définie par défaut sur 15 threads. Utilisez setThreadPoolMaxThreadCount pour modifier cette valeur :

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