Binder's threading model is designed to facilitate local function calls even though those calls might be to a remote process. Specifically, any process hosting a node must have a pool of one or more binder threads to handle transactions to nodes hosted in that process.
Synchronous and asynchronous transactions
Binder supports synchronous and asynchronous transactions. The following sections explain how each transaction type is executed.
Synchronous transactions
Synchronous transactions block until they have been executed on the node, and a reply for that transaction has been received by the caller. The following figure shows how a synchronous transaction is executed:
Figure 1. Synchronous transaction.
To execute a synchronous transaction, binder does the following:
- The threads in the target threadpool (T2 and T3) call into the kernel driver to wait for incoming work.
- The kernel receives a new transaction and wakes up a thread (T2) in the target process to handle the transaction.
- The calling thread (T1) blocks and waits for a reply.
- The target process executes the transaction and returns a reply.
- The thread in the target process (T2) calls back into the kernel driver to wait for new work.
Asynchronous transactions
Asynchronous transactions don't block for completion; the calling thread unblocks as soon as the transaction has been passed into the kernel. The following figure shows how an asynchronous transaction is executed:
Figure 2. Asynchronous transaction.
- The threads in the target threadpool (T2 and T3) call into the kernel driver to wait for incoming work.
- The kernel receives a new transaction and wakes up a thread (T2) in the target process to handle the transaction.
- The calling thread (T1) continues execution.
- The target process executes the transaction and returns a reply.
- The thread in the target process (T2) calls back into the kernel driver to wait for new work.
Identify a synchronous or asynchronous function
Functions marked as oneway
in the AIDL file are asynchronous. For example:
oneway void someCall();
If a function isn't marked as oneway
, it's a synchronous function, even if
the function returns void
.
Serialization of asynchronous transactions
Binder serializes asynchronous transactions from any single node. The following figure shows how binder serializes asynchronous transactions:
Figure 3. Serialization of asynchronous transactions.
- The threads in the target threadpool (B1 and B2) calls into the kernel driver to wait for incoming work.
- Two transactions (T1 and T2) on the same node (N1) are sent to the kernel.
- The kernel receives a new transactions and, because they are from the same node (N1), serializes them.
- Another transaction on a different node (N2) is sent to the kernel.
- The kernel receives the third transaction and wakes up a thread (B2) in the target process to handle the transaction.
- The target processes execute each transaction and returns a reply.
Nested transactions
Synchronous transactions can be nested; a thread that is handling a transaction can issue a new transaction. The nested transaction can be to a different process, or to the same process that you received the current transaction from. This behavior mimics local function calls. For example, suppose you have a function with nested functions:
def outer_function(x):
def inner_function(y):
def inner_inner_function(z):
If these are local calls, they are executed on the same thread.
Specifically, if the caller of inner_function
also happens to be the process
hosting the node that implements inner_inner_function
, the call to
inner_inner_function
is executed on the same thread.
The following figure shows how binder handles nested transactions:
Figure 4. Nested transactions.
- Thread A1 requests running
foo()
. - As part of this request, thread B1 runs
bar()
which A runs on the same thread A1.
The following figure shows thread execution if the node that implements
bar()
is in a different process:
Figure 5. Nested transactions in different processes.
- Thread A1 requests running
foo()
. - As part of this request, thread B1 runs
bar()
which runs in another thread C1.
The following figure shows how the thread reuses the same process anywhere in the transaction chain:
Figure 6. Nested transactions reusing a thread.
- Process A calls into process B.
- Process B calls into process C.
- Process C then makes a call back into process A and the kernel reuses the thread A1 in process A that is part of the transaction chain.
For asynchronous transactions, nesting doesn't play a role; the client doesn't wait for the result of an asynchronous transaction, so there is no nesting. If the handler of an asynchronous transaction makes a call into the process that issued that asynchronous transaction, that transaction can be handled on any free thread in that process.
Avoid deadlocks
The following image shows a common deadlock:
Figure 7. Common deadlock.
- Process A takes mutex MA and makes a binder call (T1) to process B which also attempts to take mutex MB.
- Simultaneously, process B takes mutex MB and makes a binder call (T2) to process A which attempts to take mutex MA.
If these transactions overlap, each transaction could potentially take a mutex in their process while waiting for the other process to release a mutex, resulting in a deadlock.
To avoid deadlocks while using binder, don't hold any lock when making a binder call.
Lock ordering rules and deadlocks
Within a single execution environment, deadlock is often avoided with a lock ordering rule. However, when making calls between processes and between codebases, especially as code gets updated, it's impossible to maintain and coordinate an ordering rule.
Single mutex and deadlocks
With nested transactions, process B can call directly back into the same thread in process A holding a mutex. Therefore, due to unexpected recursion, it's still possible to get a deadlock with a single mutex.
Synchronous calls and deadlocks
While asynchronous binder calls don't block for completion, you should also avoid holding a lock for asynchronous calls. If you hold a lock, you might experience locking issues if a one-way call is accidentally changed to a synchronous call.
Single binder thread and deadlocks
Binder's transaction model allows for reentrancy, so even if a process has a single binder thread, you still need locking. For example, suppose you're iterating over a list in a single-threaded process A. For each item in the list, you make an outgoing binder transaction. If the implementation of the function you are calling makes a new binder transaction to a node hosted in process A, that transaction is handled on the same thread that was iterating the list. If the implementation of that transaction modifies the same list, you could experience issues when you continue iterating over the list later.
Configure threadpool size
When a service has multiple clients, adding more threads to the threadpool can reduce contention and serve more calls in parallel. After you deal with concurrency correctly, you can add more threads. An issue that can be caused by adding more threads that some threads might not be used during quiet workloads.
Threads are spawned on-demand until a configured maximum After a binder thread has been spawned, it stays alive until the process hosting it ends.
The libbinder library has a default of 15 threads. Use
setThreadPoolMaxThreadCount
to change this value:
using ::android::ProcessState;
ProcessState::self()->setThreadPoolMaxThreadCount(size_t maxThreads);