处理线程

Binder 的线程处理模型旨在方便本地函数调用,即使这些调用可能是对远程进程的调用。具体而言,托管节点的任何进程都必须有一个或多个 binder 线程池来处理向该进程中托管的节点的事务。

同步和异步事务

Binder 支持同步和异步交易。以下各部分介绍了每种交易类型的执行方式。

同步交易

同步交易会一直处于阻塞状态,直到在节点上执行完毕,并且调用方收到该交易的回复。下图展示了同步事务的执行方式:

同步交易。

图 1. 同步交易。

如需执行同步事务,binder 会执行以下操作:

  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 会序列化来自任何单个节点的异步事务。下图展示了 binder 如何序列化异步事务:

异步交易的序列化。

图 3. 异步交易的序列化。

  1. 目标线程池(B1 和 B2)中的线程会调用内核驱动程序来等待传入的工作。
  2. 同一节点 (N1) 上的两个事务(T1 和 T2)被发送到内核。
  3. 内核收到新事务,由于它们来自同一节点 (N1),因此会对其进行序列化。
  4. 另一个节点 (N2) 上的另一事务被发送到内核。
  5. 内核接收第三个事务,并唤醒目标进程中的线程 (B2) 来处理该事务。
  6. 目标进程执行每项事务并返回回复。

嵌套事务

同步事务可以嵌套;处理事务的线程可以发出新事务。嵌套事务可以发送到其他进程,也可以发送到您接收当前事务的同一进程。此行为会模拟本地函数调用。例如,假设您有一个包含嵌套函数的函数:

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

如果这些是本地调用,则会在同一线程上执行。 具体而言,如果 inner_function 的调用方恰好也是实现 inner_inner_function 的节点的宿主进程,则对 inner_inner_function 的调用会在同一线程上执行。

下图展示了 binder 如何处理嵌套事务:

嵌套事务。

图 4. 嵌套事务。

  1. 线程 A1 请求运行 foo()
  2. 在此请求中,线程 B1 运行 bar(),而 A 在同一线程 A1 上运行。

下图显示了实现 bar() 的节点位于不同进程中的线程执行情况:

不同进程中的嵌套事务。

图 5. 不同进程中的嵌套事务。

  1. 线程 A1 请求运行 foo()
  2. 在此请求中,线程 B1 运行 bar(),而 bar() 在另一个线程 C1 中运行。

下图显示了线程如何在事务链中的任何位置重复使用同一进程:

重用线程的嵌套事务。

图 6. 重用线程的嵌套事务。

  1. 进程 A 调用进程 B。
  2. 进程 B 调用进程 C。
  3. 然后,进程 C 回调进程 A,内核会重用进程 A 中属于事务链的线程 A1。

对于异步交易,嵌套不起作用;客户端不会等待异步交易的结果,因此不存在嵌套。如果异步事务的处理程序调用了发出该异步事务的进程,则可以在该进程中的任何空闲线程上处理该事务。

避免死锁

下图展示了一个常见的死锁:

常见死锁。

图 7. 常见死锁。

  1. 进程 A 获取互斥锁 MA 并向进程 B 发出 binder 调用 (T1),进程 B 也尝试获取互斥锁 MB。
  2. 与此同时,进程 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);