Как работать с цепочками сообщений

Модель потоков Binder предназначена для облегчения локальных вызовов функций, даже если эти вызовы могут быть адресованы удаленному процессу. В частности, любой процесс, в котором размещен узел, должен иметь пул из одного или нескольких потоков связывателя для обработки транзакций с узлами, размещенными в этом процессе.

Синхронные и асинхронные транзакции

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 сериализует асинхронные транзакции из любого отдельного узла. На рисунке ниже показано, как Binder сериализует асинхронные транзакции.

Сериализация асинхронных транзакций.

Рисунок 3. Сериализация асинхронных транзакций.

  1. Потоки в целевом пуле потоков (B1 и B2) вызывают драйвер ядра, чтобы дождаться входящей работы.
  2. Две транзакции (T1 и T2) на одном узле (N1) отправляются в ядро.
  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 выполняется в том же потоке.

На рисунке ниже показано, как связыватель обрабатывает вложенные транзакции.

Вложенные транзакции.

Рисунок 4. Вложенные транзакции.

  1. Поток A1 запрашивает выполнение foo().
  2. В рамках этого запроса поток B1 выполняет bar(), который A выполняет в том же потоке A1.

На рисунке ниже показано выполнение потока, если узел, реализующий bar(), находится в другом процессе:

Вложенные транзакции в разных процессах.

Рисунок 5. Вложенные транзакции в разных процессах.

  1. Поток A1 запрашивает выполнение foo().
  2. В рамках этого запроса поток B1 выполняет bar(), который выполняется в другом потоке C1.

На рисунке ниже показано, как поток повторно использует один и тот же процесс в цепочке транзакций:

Вложенные транзакции, использующие один и тот же поток.

Рисунок 6. Вложенные транзакции, повторно использующие поток.

  1. Процесс А вызывает процесс Б.
  2. Процесс B вызывает процесс C.
  3. Затем процесс C вызывает процесс A, и ядро повторно использует поток A1 в процессе A, который является частью цепочки транзакций.

При асинхронных транзакциях вложенность не играет роли: клиент не ждет результата асинхронной транзакции, поэтому вложенности нет. Если обработчик асинхронной транзакции вызывает процесс, который ее инициировал, транзакция может быть обработана любым свободным потоком в этом процессе.

Как избежать взаимоблокировок

На изображении ниже показана типичная ситуация взаимной блокировки:

Распространенные причины взаимоблокировок.

Рисунок 7. Распространенные причины взаимоблокировок.

  1. Процесс A принимает блокировку MA и выполняет вызов связывателя (T1) к процессу B, который также пытается принять блокировку MB.
  2. Одновременно процесс B принимает блокировку MB и выполняет вызов механизма Binder (T2) к процессу A, который пытается принять блокировку MA.

Если эти транзакции пересекаются, каждая из них может заблокировать мьютекс в своем процессе, ожидая, пока другой процесс освободит мьютекс, что приведет к взаимной блокировке.

Чтобы избежать взаимоблокировок при использовании Binder, не удерживайте блокировку при вызове Binder.

Правила блокировки и взаимоблокировки

В одной среде выполнения взаимоблокировки часто удается избежать с помощью правила упорядочения блокировок. Однако при вызовах между процессами и базами кода, особенно по мере обновления кода, поддерживать и координировать правило упорядочения невозможно.

Один мьютекс и взаимные блокировки

При вложенных транзакциях процесс B может напрямую вызывать тот же поток в процессе A, удерживая мьютекс. Таким образом, из-за неожиданной рекурсии все ещё возможна взаимная блокировка с одним мьютексом.

Асинхронные вызовы и взаимоблокировки

Хотя асинхронные вызовы связывателя не блокируются до завершения, вам также следует избегать удержания блокировки для асинхронных вызовов. Если вы удерживаете блокировку, у вас могут возникнуть проблемы с блокировкой, если односторонний вызов случайно изменится на синхронный.

Однопоточный связыватель и взаимоблокировки

Модель транзакций Binder допускает повторный вход, поэтому даже если у процесса есть один поток Binder, блокировка все равно необходима. Например, предположим, что вы перебираете список в однопоточном процессе А. Для каждого элемента в списке вы выполняете исходящую транзакцию связывания. Если реализация вызываемой функции создает новую транзакцию связывания с узлом, размещенным в процессе А, эта транзакция обрабатывается в том же потоке, который перебирал список. Если реализация этой транзакции изменяет тот же список, у вас могут возникнуть проблемы, когда вы продолжите итерацию по списку позже.

Как настроить размер пула потоков

Если у сервиса несколько клиентов, добавление потоков в пул может снизить конкуренцию и позволить параллельно обрабатывать больше вызовов. После того, как вы правильно разберетесь с параллелизмом, вы можете добавить больше потоков. Проблема, которая может быть вызвана добавлением дополнительных потоков, которые могут не использоваться во время спокойных рабочих нагрузок.

Потоки создаются по запросу, пока не будет достигнуто заданное максимальное количество. После создания поток Binder остается активным, пока не завершится процесс, в котором он размещен.

В библиотеке libbinder по умолчанию используется 15 потоков. Чтобы изменить это значение, используйте setThreadPoolMaxThreadCount:

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