Limites de threads do Android AsyncTask?

95

Estou desenvolvendo um aplicativo onde preciso atualizar algumas informações toda vez que o usuário faz login no sistema, também uso banco de dados no telefone. Para todas essas operações (atualizações, recuperação de dados do banco de dados e etc.) eu uso tarefas assíncronas. Até agora, não vi por que não deveria usá-los, mas recentemente percebi que se eu fizer algumas operações, algumas das minhas tarefas assíncronas simplesmente param na pré-execução e não vão para doInBackground. Era muito estranho deixar assim, então desenvolvi outro aplicativo simples apenas para verificar o que estava errado. E por estranho que pareça, obtenho o mesmo comportamento quando a contagem do total de tarefas assíncronas atinge 5, a 6ª para na pré-execução.

O Android tem um limite de asyncTasks em Activity / App? Ou é apenas um bug e deve ser relatado? Alguém teve o mesmo problema e talvez tenha encontrado uma solução alternativa?

Aqui está o código:

Basta criar 5 desses tópicos para trabalhar em segundo plano:

private class LongAsync extends AsyncTask<String, Void, String>
{
    @Override
    protected void onPreExecute()
    {
        Log.d("TestBug","onPreExecute");
        isRunning = true;
    }

    @Override
    protected String doInBackground(String... params)
    {
        Log.d("TestBug","doInBackground");
        while (isRunning)
        {

        }
        return null;
    }

    @Override
    protected void onPostExecute(String result)
    {
        Log.d("TestBug","onPostExecute");
    }
}

E então crie este tópico. Ele entrará em preExecute e travará (não irá para doInBackground).

private class TestBug extends AsyncTask<String, Void, String>
{
    @Override
    protected void onPreExecute()
    {
        Log.d("TestBug","onPreExecute");

        waiting = new ProgressDialog(TestActivity.this);
        waiting.setMessage("Loading data");
        waiting.setIndeterminate(true);
        waiting.setCancelable(true);
        waiting.show();
    }

    @Override
    protected String doInBackground(String... params)
    {
        Log.d("TestBug","doInBackground");
        return null;
    }

    @Override
    protected void onPostExecute(String result)
    {
        waiting.cancel();
        Log.d("TestBug","onPostExecute");
    }
}
Mindaugas Svirskas
fonte

Respostas:

207

Todas as AsyncTasks são controladas internamente por um ThreadPoolExecutor compartilhado (estático) e um LinkedBlockingQueue . Quando você chama executeuma AsyncTask, o ThreadPoolExecutorirá executá-la quando estiver pronta em algum momento no futuro.

O 'quando estou pronto?' o comportamento de a ThreadPoolExecutoré controlado por dois parâmetros, o tamanho do pool principal e o tamanho máximo do pool . Se houver menos de encadeamentos de tamanho de pool de núcleo atualmente ativos e um novo trabalho chegar, o executor criará um novo encadeamento e o executará imediatamente. Se houver pelo menos threads de tamanho de pool de núcleo em execução, ele tentará enfileirar o trabalho e aguardar até que haja um thread inativo disponível (ou seja, até que outro trabalho seja concluído). Se não for possível colocar o trabalho na fila (a fila pode ter uma capacidade máxima), ele criará um novo encadeamento (encadeamentos de tamanho máximo de pool) para os trabalhos serem executados. Os encadeamentos ociosos não centrais podem ser encerrados de acordo com um parâmetro de tempo limite de keep-alive.

Antes do Android 1.6, o tamanho do pool principal era 1 e o tamanho máximo do pool era 10. Desde o Android 1.6, o tamanho do pool principal é 5 e o tamanho máximo do pool é 128. O tamanho da fila é 10 em ambos os casos. O tempo limite de keep-alive foi de 10 segundos antes de 2.3 e 1 segundo desde então.

Com tudo isso em mente, agora fica claro por que o AsyncTasksó aparecerá para executar 5/6 de suas tarefas. A 6ª tarefa está sendo enfileirada até que uma das outras tarefas seja concluída. Este é um bom motivo pelo qual você não deve usar AsyncTasks para operações de longa duração - isso impedirá que outras AsyncTasks sejam executadas.

Para completar, se você repetiu o exercício com mais de 6 tarefas (por exemplo, 30), verá que mais de 6 entrarão, doInBackgroundpois a fila ficará cheia e o executor será pressionado para criar mais threads de trabalho. Se você continuou com a tarefa de longa duração, verá que 20/30 tornou-se ativo, com 10 ainda na fila.

antonyt
fonte
2
"Este é um bom motivo pelo qual você não deve usar AsyncTasks para operações de longa duração" Qual é a sua recomendação para este cenário? Gerando um novo thread manualmente ou criando seu próprio serviço de executor?
user123321
2
Os executores são basicamente uma abstração em cima de threads, aliviando a necessidade de escrever códigos complexos para gerenciá-los. Ele separa suas tarefas de como deveriam ser executadas. Se o seu código depende apenas de um executor, é fácil mudar de forma transparente quantos threads são usados, etc. Não consigo pensar em um bom motivo para criar threads você mesmo, já que mesmo para tarefas simples, a quantidade de trabalho com um Executor é o mesmo, senão menos.
antonyt
37
Observe que a partir do Android 3.0+, o número padrão de AsyncTasks simultâneas foi reduzido para 1. Mais informações: developer.android.com/reference/android/os/…
Kieran
Uau, muito obrigado por uma ótima resposta. Finalmente, tenho uma explicação de por que meu código está falhando tão esporadicamente e misteriosamente.
Chris Knight
@antonyt, Mais uma dúvida, AsyncTasks canceladas, isso contará para o número de AsyncTasks? ou seja, contado em core pool sizeou maximum pool size?
nmxprime
9

@antonyt tem a resposta certa, mas caso você esteja procurando por uma solução simples, pode dar uma olhada no Needle.

Com ele você pode definir um tamanho de pool de thread customizado e, ao contrário AsyncTask, funciona em todas as versões do Android da mesma forma. Com ele você pode dizer coisas como:

Needle.onBackgroundThread().withThreadPoolSize(3).execute(new UiRelatedTask<Integer>() {
   @Override
   protected Integer doWork() {
       int result = 1+2;
       return result;
   }

   @Override
   protected void thenDoUiRelatedWork(Integer result) {
       mSomeTextView.setText("result: " + result);
   }
});

ou coisas como

Needle.onMainThread().execute(new Runnable() {
   @Override
   public void run() {
       // e.g. change one of the views
   }
}); 

Pode fazer muito mais. Confira no GitHub .

Zsolt Safrany
fonte
o último commit foi há 5 anos :(
LukaszTaraszka
5

Atualizar : desde a API 19, o tamanho do pool de thread principal foi alterado para refletir o número de CPUs no dispositivo, com um mínimo de 2 e máximo de 4 no início, enquanto aumenta para um máximo de CPU * 2 +1 - Referência

// We want at least 2 threads and at most 4 threads in the core pool,
// preferring to have 1 less than the CPU count to avoid saturating
// the CPU with background work
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;

Observe também que, embora o executor padrão de AsyncTask seja serial (executa uma tarefa por vez e na ordem em que chegam), com o método

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
        Params... params)

você pode fornecer um Executor para executar suas tarefas. Você pode fornecer ao THREAD_POOL_EXECUTOR o executor interno, mas sem serialização de tarefas, ou pode até mesmo criar seu próprio Executor e fornecê-lo aqui. No entanto, observe cuidadosamente o aviso nos Javadocs.

Aviso: Permitir que várias tarefas sejam executadas em paralelo a partir de um pool de threads geralmente não é o que se deseja, porque a ordem de sua operação não está definida. Por exemplo, se essas tarefas são usadas para modificar qualquer estado em comum (como escrever um arquivo devido a um clique de botão), não há garantias na ordem das modificações. Sem um trabalho cuidadoso, é possível, em casos raros, que a versão mais recente dos dados seja substituída por uma versão mais antiga, levando a perda de dados obscura e problemas de estabilidade. Essas alterações são executadas melhor em série; para garantir que esse trabalho seja serializado independentemente da versão da plataforma, você pode usar esta função com SERIAL_EXECUTOR.

Mais uma coisa a observar é que tanto a estrutura fornecida Executors THREAD_POOL_EXECUTOR e sua versão serial SERIAL_EXECUTOR (que é padrão para AsyncTask) são estáticos (construções de nível de classe) e, portanto, compartilhados entre todas as instâncias de AsyncTask (s) em seu processo de aplicativo.

rnk
fonte