Il modello di threading di Binder è progettato per facilitare le chiamate di funzioni locali anche se queste potrebbero essere a un processo remoto. Nello specifico, qualsiasi processo che ospita un nodo deve avere un pool di uno o più thread binder per gestire le transazioni ai nodi ospitati in quel processo.
Transazioni sincrone e asincrone
Binder supporta le transazioni sincrone e asincrone. Le sezioni seguenti spiegano come viene eseguito ogni tipo di transazione.
Transazioni sincrone
Le transazioni sincrone vengono bloccate finché non vengono eseguite sul nodo e il chiamante non riceve una risposta per la transazione. La seguente figura mostra come viene eseguita una transazione sincrona:
Figura 1. Transazione sincrona.
Per eseguire una transazione sincrona, binder esegue le seguenti operazioni:
- I thread nel pool di thread di destinazione (T2 e T3) chiamano il driver del kernel per attendere il lavoro in arrivo.
- Il kernel riceve una nuova transazione e riattiva un thread (T2) nel processo di destinazione per gestire la transazione.
- Il thread chiamante (T1) si blocca e attende una risposta.
- Il processo di destinazione esegue la transazione e restituisce una risposta.
- Il thread nel processo di destinazione (T2) richiama il driver del kernel per attendere un nuovo lavoro.
Transazioni asincrone
Le transazioni asincrone non bloccano il completamento; il thread chiamante si sblocca non appena la transazione è stata passata al kernel. La figura seguente mostra come viene eseguita una transazione asincrona:
Figura 2. Transazione asincrona.
- I thread nel pool di thread di destinazione (T2 e T3) chiamano il driver del kernel per attendere il lavoro in arrivo.
- Il kernel riceve una nuova transazione e riattiva un thread (T2) nel processo di destinazione per gestire la transazione.
- Il thread chiamante (T1) continua l'esecuzione.
- Il processo di destinazione esegue la transazione e restituisce una risposta.
- Il thread nel processo di destinazione (T2) richiama il driver del kernel per attendere un nuovo lavoro.
Identificare una funzione sincrona o asincrona
Le funzioni contrassegnate come oneway
nel file AIDL sono asincrone. Ad esempio:
oneway void someCall();
Se una funzione non è contrassegnata come oneway
, è una funzione sincrona, anche se
restituisce void
.
Serializzazione delle transazioni asincrone
Binder serializza le transazioni asincrone da qualsiasi singolo nodo. La figura seguente mostra come Binder serializza le transazioni asincrone:
Figura 3. Serializzazione delle transazioni asincrone.
- I thread nel pool di thread di destinazione (B1 e B2) chiamano il driver del kernel per attendere il lavoro in arrivo.
- Due transazioni (T1 e T2) sullo stesso nodo (N1) vengono inviate al kernel.
- Il kernel riceve nuove transazioni e, poiché provengono dallo stesso nodo (N1), le serializza.
- Un'altra transazione su un nodo diverso (N2) viene inviata al kernel.
- Il kernel riceve la terza transazione e riattiva un thread (B2) nel processo di destinazione per gestirla.
- I processi di destinazione eseguono ogni transazione e restituiscono una risposta.
Transazioni nidificate
Le transazioni sincrone possono essere nidificate. Un thread che gestisce una transazione può emetterne una nuova. La transazione nidificata può essere relativa a un processo diverso o allo stesso processo da cui hai ricevuto la transazione corrente. Questo comportamento imita le chiamate di funzioni locali. Ad esempio, supponiamo di avere una funzione con funzioni nidificate:
def outer_function(x):
def inner_function(y):
def inner_inner_function(z):
Se si tratta di chiamate locali, vengono eseguite sullo stesso thread.
Nello specifico, se il chiamante di inner_function
è anche il processo
che ospita il nodo che implementa inner_inner_function
, la chiamata a
inner_inner_function
viene eseguita sullo stesso thread.
La figura seguente mostra come il binder gestisce le transazioni nidificate:
Figura 4. Transazioni nidificate.
- Il thread A1 richiede l'esecuzione di
foo()
. - Nell'ambito di questa richiesta, il thread B1 esegue
bar()
, che A esegue sullo stesso thread A1.
La figura seguente mostra l'esecuzione dei thread se il nodo che implementa
bar()
si trova in un processo diverso:
Figura 5. Transazioni nidificate in processi diversi.
- Il thread A1 richiede l'esecuzione di
foo()
. - Nell'ambito di questa richiesta, il thread B1 esegue
bar()
, che viene eseguito in un altro thread C1.
La figura seguente mostra come il thread riutilizza lo stesso processo in qualsiasi punto della catena di transazioni:
Figura 6. Transazioni nidificate che riutilizzano un thread.
- Il processo A chiama il processo B.
- Il processo B chiama il processo C.
- Il processo C esegue quindi un callback nel processo A e il kernel riutilizza il thread A1 nel processo A che fa parte della catena di transazioni.
Per le transazioni asincrone, il nesting non svolge alcun ruolo; il client non attende il risultato di una transazione asincrona, quindi non è presente alcun nesting. Se il gestore di una transazione asincrona effettua una chiamata al processo che ha emesso la transazione asincrona, quest'ultima può essere gestita su qualsiasi thread libero nel processo.
Evitare i deadlock
L'immagine seguente mostra un deadlock comune:
Figura 7. Stallo comune.
- Il processo A acquisisce il mutex MA ed esegue una chiamata binder (T1) al processo B, che tenta anche di acquisire il mutex MB.
- Contemporaneamente, il processo B acquisisce il mutex MB ed effettua una chiamata binder (T2) al processo A, che tenta di acquisire il mutex MA.
Se queste transazioni si sovrappongono, ogni transazione potrebbe acquisire un mutex nel proprio processo in attesa che l'altro processo rilasci un mutex, con conseguente deadlock.
Per evitare deadlock durante l'utilizzo di Binder, non mantenere alcun blocco durante l'esecuzione di una chiamata Binder.
Regole di ordinamento dei blocchi e deadlock
All'interno di un singolo ambiente di esecuzione, il deadlock viene spesso evitato con una regola di ordinamento dei blocchi. Tuttavia, quando si effettuano chiamate tra processi e tra basi di codice, soprattutto quando il codice viene aggiornato, è impossibile mantenere e coordinare una regola di ordinamento.
Mutex singolo e deadlock
Con le transazioni nidificate, il processo B può richiamare direttamente lo stesso thread nel processo A che contiene un mutex. Pertanto, a causa di una ricorsione imprevista, è comunque possibile ottenere un deadlock con un singolo mutex.
Chiamate sincrone e deadlock
Sebbene le chiamate asincrone del binder non blocchino il completamento, devi anche evitare di mantenere un blocco per le chiamate asincrone. Se hai una prenotazione, potresti riscontrare problemi di blocco se una chiamata unidirezionale viene modificata accidentalmente in una chiamata sincrona.
Singolo thread del binder e deadlock
Il modello di transazione di Binder consente il rientro, quindi anche se un processo ha un singolo thread Binder, è comunque necessario il blocco. Ad esempio, supponiamo di eseguire l'iterazione su un elenco in un processo A a thread singolo. Per ogni elemento dell'elenco, esegui una transazione di binder in uscita. Se l'implementazione della funzione che stai chiamando crea una nuova transazione binder in un nodo ospitato nel processo A, questa transazione viene gestita nello stesso thread che stava iterando l'elenco. Se l'implementazione di questa transazione modifica lo stesso elenco, potresti riscontrare problemi quando continui a scorrere l'elenco in un secondo momento.
Configurare le dimensioni del pool di thread
Quando un servizio ha più client, l'aggiunta di più thread al pool di thread può ridurre la contesa e gestire più chiamate in parallelo. Dopo aver gestito correttamente la concorrenza, puoi aggiungere altri thread. Un problema che può essere causato dall'aggiunta di più thread che potrebbero non essere utilizzati durante i workload inattivi.
I thread vengono generati su richiesta fino a un massimo configurato. Una volta generato un thread binder, rimane attivo fino alla chiusura del processo che lo ospita.
La libreria libbinder ha un valore predefinito di 15 thread. Utilizza
setThreadPoolMaxThreadCount
per modificare questo valore:
using ::android::ProcessState;
ProcessState::self()->setThreadPoolMaxThreadCount(size_t maxThreads);