Gerenciar linhas de execução

O modelo de threading do Binder foi projetado para facilitar chamadas de função local, mesmo que elas sejam para um processo remoto. Especificamente, qualquer processo que hospede um nó precisa ter um pool de uma ou mais linhas de execução do binder para processar transações para nós hospedados nesse processo.

Transações síncronas e assíncronas

O Binder oferece suporte a transações síncronas e assíncronas. As seções a seguir explicam como cada tipo de transação é executado.

Transações síncronas

As transações síncronas são bloqueadas até serem executadas no nó, e uma resposta para essa transação é recebida pelo autor da chamada. A figura a seguir mostra como uma transação síncrona é executada:

Transação síncrona.

Figura 1. Transação síncrona.

Para executar uma transação síncrona, o binder faz o seguinte:

  1. As linhas de execução no pool de linhas de execução de destino (T2 e T3) chamam o driver do kernel para aguardar o trabalho recebido.
  2. O kernel recebe uma nova transação e ativa uma linha de execução (T2) no processo de destino para processar a transação.
  3. A linha de execução de chamada (T1) bloqueia e aguarda uma resposta.
  4. O processo de destino executa a transação e retorna uma resposta.
  5. A linha de execução no processo de destino (T2) faz uma nova chamada ao driver do kernel para aguardar um novo trabalho.

Transações assíncronas

As transações assíncronas não bloqueiam a conclusão. A linha de execução da chamada é desbloqueada assim que a transação é transmitida ao kernel. A figura a seguir mostra como uma transação assíncrona é executada:

Transação assíncrona.

Figura 2. Transação assíncrona.

  1. As linhas de execução no pool de linhas de execução de destino (T2 e T3) chamam o driver do kernel para aguardar o trabalho recebido.
  2. O kernel recebe uma nova transação e ativa uma linha de execução (T2) no processo de destino para processar a transação.
  3. A execução da linha de execução de chamada (T1) continua.
  4. O processo de destino executa a transação e retorna uma resposta.
  5. A linha de execução no processo de destino (T2) faz uma nova chamada ao driver do kernel para aguardar um novo trabalho.

Identificar uma função síncrona ou assíncrona

As funções marcadas como oneway no arquivo AIDL são assíncronas. Exemplo:

oneway void someCall();

Se uma função não for marcada como oneway, ela será síncrona, mesmo que retorne void.

Serialização de transações assíncronas

O Binder serializa transações assíncronas de qualquer nó único. A figura a seguir mostra como o binder serializa transações assíncronas:

Serialização de transações assíncronas.

Figura 3. Serialização de transações assíncronas.

  1. As linhas de execução no pool de linhas de execução de destino (B1 e B2) chamam o driver do kernel para aguardar o trabalho recebido.
  2. Duas transações (T1 e T2) no mesmo nó (N1) são enviadas ao kernel.
  3. O kernel recebe novas transações e, como elas são do mesmo nó (N1), as serializa.
  4. Outra transação em um nó diferente (N2) é enviada ao kernel.
  5. O kernel recebe a terceira transação e ativa uma linha de execução (B2) no processo de destino para processar a transação.
  6. Os processos de destino executam cada transação e retornam uma resposta.

Transações aninhadas

As transações síncronas podem ser aninhadas. Uma linha de execução que está processando uma transação pode emitir uma nova transação. A transação aninhada pode ser para um processo diferente ou para o mesmo processo de que você recebeu a transação atual. Esse comportamento imita as chamadas de função local. Por exemplo, suponha que você tenha uma função com funções aninhadas:

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

Se forem chamadas locais, elas serão executadas na mesma linha de execução. Especificamente, se o autor da chamada de inner_function também for o processo que hospeda o nó que implementa inner_inner_function, a chamada para inner_inner_function será executada na mesma linha de execução.

A figura a seguir mostra como o binder processa transações aninhadas:

Transações aninhadas.

Figura 4. Transações aninhadas.

  1. A linha de execução A1 solicita a execução de foo().
  2. Como parte dessa solicitação, a linha de execução B1 executa bar(), que A executa na mesma linha de execução A1.

A figura a seguir mostra a execução da linha de execução se o nó que implementa bar() estiver em um processo diferente:

Transações aninhadas em processos diferentes.

Figura 5. Transações aninhadas em processos diferentes.

  1. A linha de execução A1 solicita a execução de foo().
  2. Como parte dessa solicitação, a linha de execução B1 executa bar(), que é executado em outra linha de execução C1.

A figura a seguir mostra como a linha de execução reutiliza o mesmo processo em qualquer lugar da cadeia de transações:

Transações aninhadas reutilizando uma linha de execução.

Figura 6. Transações aninhadas reutilizando uma linha de execução.

  1. O processo A faz uma chamada para o processo B.
  2. O processo B chama o processo C.
  3. Em seguida, o processo C faz uma chamada de volta ao processo A, e o kernel reutiliza a linha de execução A1 no processo A, que faz parte da cadeia de transações.

Para transações assíncronas, o aninhamento não tem importância. O cliente não aguarda o resultado de uma transação assíncrona, então não há aninhamento. Se o manipulador de uma transação assíncrona fizer uma chamada para o processo que emitiu essa transação, ela poderá ser processada em qualquer encadeamento livre nesse processo.

Evitar impasses

A imagem a seguir mostra um deadlock comum:

Impasse comum.

Figura 7. Impasse comum.

  1. O processo A usa o mutex MA e faz uma chamada de vinculação (T1) para o processo B, que também tenta usar o mutex MB.
  2. Simultaneamente, o processo B usa o mutex MB e faz uma chamada de binder (T2) para o processo A, que tenta usar o mutex MA.

Se essas transações se sobrepuserem, cada uma poderá usar um mutex no processo enquanto espera que o outro processo libere um mutex, resultando em um deadlock.

Para evitar impasses ao usar o binder, não mantenha nenhum bloqueio ao fazer uma chamada de binder.

Regras de ordenação de bloqueios e deadlocks

Em um único ambiente de execução, o deadlock geralmente é evitado com uma regra de ordenação de bloqueio. No entanto, ao fazer chamadas entre processos e entre bases de código, principalmente quando o código é atualizado, é impossível manter e coordenar uma regra de ordenação.

Mutex único e deadlocks

Com transações aninhadas, o processo B pode chamar diretamente de volta para a mesma linha de execução no processo A que contém um mutex. Portanto, devido a uma recursão inesperada, ainda é possível ter um deadlock com um único mutex.

Chamadas síncronas e impasses

Embora as chamadas de binder assíncronas não bloqueiem a conclusão, evite manter um bloqueio para chamadas assíncronas. Se você mantiver um bloqueio, poderá ter problemas se uma chamada unidirecional for alterada acidentalmente para uma chamada síncrona.

Uma única linha de execução do binder e deadlocks

O modelo de transação do Binder permite a reentrada. Portanto, mesmo que um processo tenha uma única linha de execução do Binder, ainda é necessário o bloqueio. Por exemplo, suponha que você esteja iterando uma lista em um processo A de linha de execução única. Para cada item na lista, você faz uma transação de binder de saída. Se a implementação da função que você está chamando fizer uma nova transação de binder para um nó hospedado no processo A, essa transação será processada na mesma linha de execução que estava iterando a lista. Se a implementação dessa transação modificar a mesma lista, você poderá ter problemas ao continuar iterando sobre ela mais tarde.

Configurar o tamanho do pool de linhas de execução

Quando um serviço tem vários clientes, adicionar mais linhas de execução ao pool pode reduzir a disputa e atender mais chamadas em paralelo. Depois de lidar com a simultaneidade corretamente, você pode adicionar mais linhas de execução. Um problema que pode ser causado pela adição de mais linhas de execução que podem não ser usadas durante cargas de trabalho inativas.

As linhas de execução são geradas sob demanda até um máximo configurado. Depois que uma linha de execução do binder é gerada, ela permanece ativa até que o processo que a hospeda seja encerrado.

A biblioteca libbinder tem um padrão de 15 linhas de execução. Use setThreadPoolMaxThreadCount para mudar esse valor:

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