Das Threading-Modell von Binder ist so konzipiert, dass lokale Funktionsaufrufe auch dann möglich sind, wenn diese Aufrufe an einen Remote-Prozess gerichtet sind. Insbesondere muss jeder Prozess, in dem ein Knoten gehostet wird, einen Pool mit mindestens einem Binder-Thread haben, um Transaktionen zu Knoten zu verarbeiten, die in diesem Prozess gehostet werden.
Synchrone und asynchrone Transaktionen
Binder unterstützt synchrone und asynchrone Transaktionen. In den folgenden Abschnitten wird beschrieben, wie die einzelnen Transaktionstypen ausgeführt werden.
Synchrone Transaktionen
Synchrone Transaktionen werden blockiert, bis sie auf dem Knoten ausgeführt wurden und der Aufrufer eine Antwort für diese Transaktion erhalten hat. Die folgende Abbildung zeigt, wie eine synchrone Transaktion ausgeführt wird:
Abbildung 1: Synchrone Transaktion.
So führt Binder eine synchrone Transaktion aus:
- Die Threads im Ziel-Threadpool (T2 und T3) rufen den Kernel-Treiber auf, um auf eingehende Arbeit zu warten.
- Der Kernel empfängt eine neue Transaktion und reaktiviert einen Thread (T2) im Zielprozess, um die Transaktion zu verarbeiten.
- Der aufrufende Thread (T1) wird blockiert und wartet auf eine Antwort.
- Der Zielprozess führt die Transaktion aus und gibt eine Antwort zurück.
- Der Thread im Zielprozess (T2) ruft den Kerneltreiber zurück, um auf neue Arbeit zu warten.
Asynchrone Transaktionen
Asynchrone Transaktionen werden nicht blockiert, bis sie abgeschlossen sind. Der aufrufende Thread wird entsperrt, sobald die Transaktion an den Kernel übergeben wurde. Die folgende Abbildung zeigt, wie eine asynchrone Transaktion ausgeführt wird:
Abbildung 2: Asynchrone Transaktion.
- Die Threads im Ziel-Threadpool (T2 und T3) rufen den Kernel-Treiber auf, um auf eingehende Arbeit zu warten.
- Der Kernel empfängt eine neue Transaktion und reaktiviert einen Thread (T2) im Zielprozess, um die Transaktion zu verarbeiten.
- Der aufrufende Thread (T1) wird weiter ausgeführt.
- Der Zielprozess führt die Transaktion aus und gibt eine Antwort zurück.
- Der Thread im Zielprozess (T2) ruft den Kerneltreiber zurück, um auf neue Arbeit zu warten.
Synchrone oder asynchrone Funktion identifizieren
Funktionen, die in der AIDL-Datei mit oneway
gekennzeichnet sind, sind asynchron. Beispiel:
oneway void someCall();
Wenn eine Funktion nicht als oneway
gekennzeichnet ist, handelt es sich um eine synchrone Funktion, auch wenn die Funktion void
zurückgibt.
Serialisierung asynchroner Transaktionen
Binder serialisiert asynchrone Transaktionen von einem beliebigen einzelnen Knoten. Die folgende Abbildung zeigt, wie Binder asynchrone Transaktionen serialisiert:
Abbildung 3: Serialisierung asynchroner Transaktionen.
- Die Threads im Ziel-Threadpool (B1 und B2) rufen den Kerneltreiber auf, um auf eingehende Arbeit zu warten.
- Zwei Transaktionen (T1 und T2) auf demselben Knoten (N1) werden an den Kernel gesendet.
- Der Kernel empfängt neue Transaktionen und serialisiert sie, da sie vom selben Knoten (N1) stammen.
- Eine weitere Transaktion auf einem anderen Knoten (N2) wird an den Kernel gesendet.
- Der Kernel empfängt die dritte Transaktion und aktiviert einen Thread (B2) im Zielprozess, um die Transaktion zu verarbeiten.
- Die Zielprozesse führen jede Transaktion aus und geben eine Antwort zurück.
Verschachtelte Transaktionen
Synchrone Transaktionen können verschachtelt werden. Ein Thread, der eine Transaktion verarbeitet, kann eine neue Transaktion ausgeben. Die verschachtelte Transaktion kann an einen anderen Prozess oder an denselben Prozess gesendet werden, von dem Sie die aktuelle Transaktion erhalten haben. Dieses Verhalten ähnelt lokalen Funktionsaufrufen. Angenommen, Sie haben eine Funktion mit verschachtelten Funktionen:
def outer_function(x):
def inner_function(y):
def inner_inner_function(z):
Wenn es sich um lokale Aufrufe handelt, werden sie im selben Thread ausgeführt.
Wenn der Aufrufer von inner_function
auch der Prozess ist, der den Knoten hostet, der inner_inner_function
implementiert, wird der Aufruf von inner_inner_function
im selben Thread ausgeführt.
Die folgende Abbildung zeigt, wie Binder verschachtelte Transaktionen verarbeitet:
Abbildung 4: Verschachtelte Transaktionen.
- Thread A1 fordert die Ausführung von
foo()
an. - Im Rahmen dieser Anfrage wird Thread B1 ausgeführt.
bar()
wird auf demselben Thread A1 wie A ausgeführt.
Die folgende Abbildung zeigt die Ausführung von Threads, wenn sich der Knoten, der bar()
implementiert, in einem anderen Prozess befindet:
Abbildung 5: Geschachtelte Transaktionen in verschiedenen Prozessen.
- Thread A1 fordert die Ausführung von
foo()
an. - Im Rahmen dieser Anfrage wird Thread B1 ausgeführt, in dem
bar()
ausgeführt wird, das in einem anderen Thread C1 ausgeführt wird.
Die folgende Abbildung zeigt, wie der Thread denselben Prozess an jeder Stelle in der Transaktionskette wiederverwendet:
Abbildung 6 Geschachtelte Transaktionen, die einen Thread wiederverwenden.
- Prozess A ruft Prozess B auf.
- Prozess B ruft Prozess C auf.
- Prozess C ruft dann Prozess A zurück und der Kernel verwendet den Thread A1 in Prozess A wieder, der Teil der Transaktionskette ist.
Bei asynchronen Transaktionen spielt die Verschachtelung keine Rolle. Der Client wartet nicht auf das Ergebnis einer asynchronen Transaktion, sodass es keine Verschachtelung gibt. Wenn der Handler einer asynchronen Transaktion einen Aufruf in den Prozess ausgibt, der diese asynchrone Transaktion ausgegeben hat, kann diese Transaktion auf einem beliebigen kostenlosen Thread in diesem Prozess verarbeitet werden.
Deadlocks vermeiden
Das folgende Bild zeigt einen häufigen Deadlock:
Abbildung 7. Allgemeiner Deadlock.
- Prozess A übernimmt den Mutex MA und führt einen Binder-Aufruf (T1) an Prozess B aus, der ebenfalls versucht, den Mutex MB zu übernehmen.
- Gleichzeitig ruft Prozess B den Mutex MB ab und führt einen Binder-Aufruf (T2) an Prozess A aus, der versucht, den Mutex MA abzurufen.
Wenn sich diese Transaktionen überschneiden, kann jede Transaktion potenziell einen Mutex in ihrem Prozess belegen, während sie darauf wartet, dass der andere Prozess einen Mutex freigibt. Dies führt zu einem Deadlock.
Um Deadlocks bei der Verwendung von Binder zu vermeiden, sollten Sie beim Ausführen eines Binder-Aufrufs keine Sperre halten.
Regeln für die Sperrenreihenfolge und Deadlocks
In einer einzelnen Ausführungsumgebung wird ein Deadlock oft durch eine Regel zur Sperrreihenfolge vermieden. Bei Aufrufen zwischen Prozessen und zwischen Codebases, insbesondere wenn Code aktualisiert wird, ist es jedoch unmöglich, eine Sortierungsregel aufrechtzuerhalten und zu koordinieren.
Einzelner Mutex und Deadlocks
Bei verschachtelten Transaktionen kann Prozess B direkt in denselben Thread in Prozess A zurückkehren, der ein Mutex enthält. Daher ist es aufgrund unerwarteter Rekursion immer noch möglich, mit einem einzelnen Mutex einen Deadlock zu erhalten.
Synchrone Aufrufe und Deadlocks
Asynchrone Binder-Aufrufe blockieren nicht, bis sie abgeschlossen sind. Sie sollten jedoch auch vermeiden, eine Sperre für asynchrone Aufrufe zu halten. Wenn Sie ein Schloss halten, kann es zu Problemen mit der Verriegelung kommen, wenn ein unidirektionaler Anruf versehentlich in einen synchronen Anruf geändert wird.
Einzelner Binder-Thread und Deadlocks
Das Transaktionsmodell von Binder ermöglicht Reentrancy. Selbst wenn ein Prozess also nur einen Binder-Thread hat, ist eine Sperrung erforderlich. Angenommen, Sie durchlaufen eine Liste in einem Single-Threaded-Prozess A. Für jedes Element in der Liste führen Sie eine ausgehende Binder-Transaktion aus. Wenn bei der Implementierung der Funktion, die Sie aufrufen, eine neue Binder-Transaktion für einen Knoten erstellt wird, der in Prozess A gehostet wird, wird diese Transaktion auf demselben Thread verarbeitet, der die Liste durchläuft. Wenn durch die Implementierung dieser Transaktion dieselbe Liste geändert wird, kann es zu Problemen kommen, wenn Sie später mit der Iteration über die Liste fortfahren.
Threadpool-Größe konfigurieren
Wenn ein Dienst mehrere Clients hat, kann das Hinzufügen weiterer Threads zum Threadpool die Konflikte verringern und mehr Aufrufe parallel verarbeiten. Wenn Sie die Parallelität richtig behandelt haben, können Sie weitere Threads hinzufügen. Ein Problem, das durch das Hinzufügen weiterer Threads verursacht werden kann, die bei geringer Arbeitslast möglicherweise nicht verwendet werden.
Threads werden bei Bedarf erstellt, bis ein konfiguriertes Maximum erreicht ist. Nachdem ein Binder-Thread erstellt wurde, bleibt er aktiv, bis der Prozess, in dem er ausgeführt wird, beendet wird.
Die libbinder-Bibliothek hat standardmäßig 15 Threads. So ändern Sie diesen Wert mit setThreadPoolMaxThreadCount
:
using ::android::ProcessState;
ProcessState::self()->setThreadPoolMaxThreadCount(size_t maxThreads);