スレッドを処理する

Binder のスレッド モデルは、ローカル関数呼び出しがリモート プロセスへの呼び出しであっても、ローカル関数呼び出しを容易にするように設計されています。具体的には、ノードをホストするプロセスには、そのプロセスでホストされているノードへのトランザクションを処理するための 1 つ以上のバインダ スレッドのプールが必要です。

同期トランザクションと非同期トランザクション

Binder は同期トランザクションと非同期トランザクションをサポートしています。以降のセクションでは、各トランザクション タイプがどのように実行されるかについて説明します。

同期トランザクション

同期トランザクションは、ノードで実行され、そのトランザクションの返信が呼び出し元に届くまでブロックされます。次の図は、同期トランザクションの実行方法を示しています。

同期トランザクション。

図 1. 同期トランザクション。

同期トランザクションを実行するために、バインダーは次の処理を行います。

  1. ターゲット スレッドプール(T2 と T3)のスレッドは、カーネル ドライバを呼び出して、受信した作業を待ちます。
  2. カーネルは新しいトランザクションを受信し、ターゲット プロセスのスレッド(T2)を起動してトランザクションを処理します。
  3. 呼び出しスレッド(T1)はブロックされ、返信を待ちます。
  4. ターゲット プロセスがトランザクションを実行し、返信を返します。
  5. ターゲット プロセス(T2)のスレッドは、カーネル ドライバにコールバックして新しい作業を待ちます。

非同期トランザクション

非同期トランザクションは完了をブロックしません。トランザクションがカーネルに渡されるとすぐに、呼び出し元のスレッドのブロックが解除されます。次の図は、非同期トランザクションの実行方法を示しています。

非同期トランザクション。

図 2. 非同期トランザクション。

  1. ターゲット スレッドプール(T2 と T3)のスレッドは、カーネル ドライバを呼び出して、受信した作業を待ちます。
  2. カーネルは新しいトランザクションを受信し、ターゲット プロセスのスレッド(T2)を起動してトランザクションを処理します。
  3. 呼び出しスレッド(T1)は実行を続行します。
  4. ターゲット プロセスがトランザクションを実行し、返信を返します。
  5. ターゲット プロセス(T2)のスレッドは、カーネル ドライバにコールバックして新しい作業を待ちます。

同期関数または非同期関数を特定する

AIDL ファイルで oneway とマークされた関数は非同期です。次に例を示します。

oneway void someCall();

関数が oneway としてマークされていない場合、関数が void を返しても、その関数は同期関数です。

非同期トランザクションのシリアル化

Binder は、単一ノードからの非同期トランザクションをシリアル化します。次の図は、バインダが非同期トランザクションをシリアル化する方法を示しています。

非同期トランザクションのシリアル化。

図 3. 非同期トランザクションのシリアル化。

  1. ターゲット スレッドプール(B1 と B2)のスレッドは、カーネル ドライバを呼び出して、着信した作業を待機します。
  2. 同じノード(N1)上の 2 つのトランザクション(T1 と T2)がカーネルに送信されます。
  3. カーネルは新しいトランザクションを受け取り、それらが同じノード(N1)からのものであるため、シリアル化します。
  4. 別のノード(N2)での別のトランザクションがカーネルに送信されます。
  5. カーネルは 3 番目のトランザクションを受信し、ターゲット プロセスのスレッド(B2)を起動してトランザクションを処理します。
  6. ターゲット プロセスは各トランザクションを実行し、返信を返します。

ネストされたトランザクション

同期トランザクションはネストできます。トランザクションを処理しているスレッドは、新しいトランザクションを発行できます。ネストされたトランザクションは、別のプロセスに送信することも、現在のトランザクションを受信した同じプロセスに送信することもできます。この動作は、ローカル関数呼び出しを模倣したものです。たとえば、次のようなネストされた関数を含む関数があるとします。

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

これらがローカル呼び出しの場合、同じスレッドで実行されます。具体的には、inner_function の呼び出し元が inner_inner_function を実装するノードをホストするプロセスでもある場合、inner_inner_function の呼び出しは同じスレッドで実行されます。

次の図は、バインダがネストされたトランザクションを処理する方法を示しています。

ネストされたトランザクション。

図 4. ネストされたトランザクション。

  1. スレッド A1 が foo() の実行をリクエストします。
  2. このリクエストの一部として、スレッド B1 は bar() を実行します。これは、A が同じスレッド A1 で実行するものです。

次の図は、bar() を実装するノードが別のプロセスにある場合のスレッド実行を示しています。

異なるプロセスでのネストされたトランザクション。

図 5. 異なるプロセスでのネストされたトランザクション。

  1. スレッド A1 が foo() の実行をリクエストします。
  2. このリクエストの一部として、スレッド B1 は別のスレッド C1 で実行される bar() を実行します。

次の図は、トランザクション チェーンのどこでもスレッドが同じプロセスを再利用する方法を示しています。

スレッドを再利用するネストされたトランザクション。

図 6. スレッドを再利用するネストされたトランザクション。

  1. プロセス A がプロセス B を呼び出します。
  2. プロセス B がプロセス C を呼び出します。
  3. プロセス C はプロセス A にコールバックし、カーネルはトランザクション チェーンの一部であるプロセス A のスレッド A1 を再利用します。

非同期トランザクションの場合、ネストは役割を果たしません。クライアントは非同期トランザクションの結果を待機しないため、ネストはありません。非同期トランザクションのハンドラが、その非同期トランザクションを発行したプロセスを呼び出す場合、そのトランザクションは、そのプロセスの任意の空きスレッドで処理できます。

デッドロックを回避する

次の図は、一般的なデッドロックを示しています。

一般的なデッドロック。

図 7. 一般的なデッドロック。

  1. プロセス A がミューテックス MA を取得し、ミューテックス MB の取得も試みるプロセス B にバインダー呼び出し(T1)を行います。
  2. 同時に、プロセス B はミューテックス MB を取得し、ミューテックス MA の取得を試みるプロセス A にバインダ呼び出し(T2)を行います。

これらのトランザクションが重複すると、各トランザクションがプロセスでミューテックスを取得し、他のプロセスがミューテックスを解放するのを待機する可能性があります。これにより、デッドロックが発生します。

バインダの使用中にデッドロックが発生しないようにするには、バインダ呼び出しを行うときにロックを保持しないでください。

ロックの順序付けルールとデッドロック

単一の実行環境では、ロック順序付けルールによってデッドロックが回避されることがよくあります。ただし、プロセス間やコードベース間で呼び出しを行う場合、特にコードが更新されると、順序付けルールを維持して調整することはできません。

単一のミューテックスとデッドロック

ネストされたトランザクションでは、プロセス B は、ミューテックスを保持しているプロセス A の同じスレッドに直接コールバックできます。したがって、予期しない再帰により、単一のミューテックスでデッドロックが発生する可能性があります。

同期呼び出しとデッドロック

非同期バインダ呼び出しは完了をブロックしませんが、非同期呼び出しでロックを保持することも避ける必要があります。ロックを保持している場合、一方向の呼び出しが誤って同期呼び出しに変更されると、ロックの問題が発生する可能性があります。

単一のバインダ スレッドとデッドロック

Binder のトランザクション モデルでは再入が可能であるため、プロセスに単一の Binder スレッドがある場合でも、ロックが必要です。たとえば、シングル スレッド プロセス A でリストを反復処理しているとします。リスト内の各アイテムに対して、バインダの送信トランザクションを行います。呼び出す関数の実装がプロセス A でホストされているノードへの新しいバインダ トランザクションを作成する場合、そのトランザクションはリストを反復処理していた同じスレッドで処理されます。そのトランザクションの実装で同じリストが変更されると、後でリストの反復処理を続行するときに問題が発生する可能性があります。

スレッドプールのサイズを構成する

サービスに複数のクライアントがある場合、スレッドプールにスレッドを追加すると、競合が減少し、より多くの呼び出しを並行して処理できます。同時実行を正しく処理したら、スレッドを追加できます。スレッドを追加すると、一部のスレッドが静かなワークロード中に使用されない可能性がある問題。

スレッドは、構成された最大数に達するまでオンデマンドで生成されます。バインダ スレッドが生成されると、それをホストするプロセスが終了するまで存続します。

libbinder ライブラリのデフォルトは 15 スレッドです。この値を変更するには、setThreadPoolMaxThreadCount を使用します。

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