Binder 的线程处理模型旨在方便本地函数调用,即使这些调用可能是对远程进程的调用。具体而言,托管节点的任何进程都必须有一个或多个 binder 线程池来处理向该进程中托管的节点的事务。
同步和异步事务
Binder 支持同步和异步交易。以下各部分介绍了每种交易类型的执行方式。
同步交易
同步交易会一直处于阻塞状态,直到在节点上执行完毕,并且调用方收到该交易的回复。下图展示了同步事务的执行方式:
图 1. 同步交易。
如需执行同步事务,binder 会执行以下操作:
- 目标线程池(T2 和 T3)中的线程会调用内核驱动程序来等待传入的工作。
- 内核接收到新事务,并唤醒目标进程中的线程 (T2) 来处理该事务。
- 调用线程 (T1) 会阻塞并等待回复。
- 目标进程执行交易并返回回复。
- 目标进程 (T2) 中的线程会回调到内核驱动程序中,以等待新工作。
异步交易
异步事务不会阻塞以等待完成;一旦事务传递到内核,调用线程就会解除阻塞。下图展示了如何执行异步交易:
图 2. 异步交易。
- 目标线程池(T2 和 T3)中的线程会调用内核驱动程序来等待传入的工作。
- 内核接收到新事务,并唤醒目标进程中的线程 (T2) 来处理该事务。
- 调用线程 (T1) 继续执行。
- 目标进程执行交易并返回回复。
- 目标进程 (T2) 中的线程会回调到内核驱动程序中,以等待新工作。
识别同步函数或异步函数
在 AIDL 文件中标记为 oneway
的函数是异步的。例如:
oneway void someCall();
如果函数未标记为 oneway
,则即使该函数返回 void
,它也是同步函数。
异步交易的序列化
Binder 会序列化来自任何单个节点的异步事务。下图展示了 binder 如何序列化异步事务:
图 3. 异步交易的序列化。
- 目标线程池(B1 和 B2)中的线程会调用内核驱动程序来等待传入的工作。
- 同一节点 (N1) 上的两个事务(T1 和 T2)被发送到内核。
- 内核收到新事务,由于它们来自同一节点 (N1),因此会对其进行序列化。
- 另一个节点 (N2) 上的另一事务被发送到内核。
- 内核接收第三个事务,并唤醒目标进程中的线程 (B2) 来处理该事务。
- 目标进程执行每项事务并返回回复。
嵌套事务
同步事务可以嵌套;处理事务的线程可以发出新事务。嵌套事务可以发送到其他进程,也可以发送到您接收当前事务的同一进程。此行为会模拟本地函数调用。例如,假设您有一个包含嵌套函数的函数:
def outer_function(x):
def inner_function(y):
def inner_inner_function(z):
如果这些是本地调用,则会在同一线程上执行。
具体而言,如果 inner_function
的调用方恰好也是实现 inner_inner_function
的节点的宿主进程,则对 inner_inner_function
的调用会在同一线程上执行。
下图展示了 binder 如何处理嵌套事务:
图 4. 嵌套事务。
- 线程 A1 请求运行
foo()
。 - 在此请求中,线程 B1 运行
bar()
,而 A 在同一线程 A1 上运行。
下图显示了实现 bar()
的节点位于不同进程中的线程执行情况:
图 5. 不同进程中的嵌套事务。
- 线程 A1 请求运行
foo()
。 - 在此请求中,线程 B1 运行
bar()
,而bar()
在另一个线程 C1 中运行。
下图显示了线程如何在事务链中的任何位置重复使用同一进程:
图 6. 重用线程的嵌套事务。
- 进程 A 调用进程 B。
- 进程 B 调用进程 C。
- 然后,进程 C 回调进程 A,内核会重用进程 A 中属于事务链的线程 A1。
对于异步交易,嵌套不起作用;客户端不会等待异步交易的结果,因此不存在嵌套。如果异步事务的处理程序调用了发出该异步事务的进程,则可以在该进程中的任何空闲线程上处理该事务。
避免死锁
下图展示了一个常见的死锁:
图 7. 常见死锁。
- 进程 A 获取互斥锁 MA 并向进程 B 发出 binder 调用 (T1),进程 B 也尝试获取互斥锁 MB。
- 与此同时,进程 B 获取互斥锁 MB 并向进程 A 发出 binder 调用 (T2),进程 A 尝试获取互斥锁 MA。
如果这些事务重叠,每个事务都可能会在其进程中获取互斥锁,同时等待另一个进程释放互斥锁,从而导致死锁。
为避免在使用 binder 时出现死锁,请勿在进行 binder 调用时持有任何锁。
锁定顺序规则和死锁
在单个执行环境中,通常可以通过锁定顺序规则来避免死锁。不过,在进程之间和代码库之间进行调用时,尤其是在代码更新时,无法维护和协调排序规则。
单个互斥锁和死锁
借助嵌套事务,进程 B 可以直接调用回进程 A 中持有互斥锁的同一线程。因此,由于意外的递归,仍有可能因单个互斥锁而发生死锁。
同步调用和死锁
虽然异步 binder 调用不会阻塞以等待完成,但您也应避免为异步调用持有锁。如果您持有锁,则可能会遇到锁定问题,因为单向调用会被意外更改为同步调用。
单个 binder 线程和死锁
Binder 的事务模型允许重入,因此即使进程只有一个 binder 线程,您仍然需要锁定。例如,假设您正在单线程进程 A 中迭代处理一个列表。对于列表中的每个项目,您都会进行一次传出 binder 交易。如果您调用的函数的实现向进程 A 中托管的节点发起了新的 binder 事务,则该事务会在迭代列表的同一线程上处理。如果该事务的实现修改了同一列表,那么您稍后继续迭代该列表时可能会遇到问题。
配置线程池大小
当服务有多个客户端时,向线程池添加更多线程可以减少争用并并行处理更多调用。正确处理并发问题后,您可以添加更多线程。添加更多线程可能会导致一些线程在工作负载较少时未被使用。
系统会根据需要生成线程,直到达到配置的最大值。生成绑定器线程后,该线程会一直保持活跃状态,直到托管它的进程结束。
libbinder 库的默认线程数为 15。使用 setThreadPoolMaxThreadCount
更改此值:
using ::android::ProcessState;
ProcessState::self()->setThreadPoolMaxThreadCount(size_t maxThreads);