Limiti dei thread AsyncTask Android?


95

Sto sviluppando un'applicazione in cui devo aggiornare alcune informazioni ogni volta che l'utente accede al sistema, utilizzo anche il database nel telefono. Per tutte quelle operazioni (aggiornamenti, recupero di dati da db e così via) utilizzo attività asincrone. Fino ad ora non vedevo perché non dovrei usarli, ma recentemente ho riscontrato che se eseguo alcune operazioni alcune delle mie attività asincrone si fermano semplicemente in pre-esecuzione e non passano a doInBackground. Era troppo strano per lasciarlo così, quindi ho sviluppato un'altra semplice applicazione solo per verificare cosa c'è che non va. E abbastanza strano, ottengo lo stesso comportamento quando il conteggio delle attività asincrone totali raggiunge 5, il sesto si ferma in pre-esecuzione.

Android ha un limite di asyncTask su attività / app? O è solo un bug e dovrebbe essere segnalato? Qualcuno ha riscontrato lo stesso problema e forse ha trovato una soluzione alternativa?

Ecco il codice:

Crea semplicemente 5 di questi thread per lavorare in background:

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 poi crea questo thread. Entrerà in preExecute e si bloccherà (non andrà a 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");
    }
}

Risposte:


207

Tutti gli AsyncTask sono controllati internamente da un ThreadPoolExecutor (statico) condiviso e da un LinkedBlockingQueue . Quando chiami executesu un AsyncTask, lo ThreadPoolExecutoreseguirà quando sarà pronto in futuro.

Il "quando sono pronto?" il comportamento di a ThreadPoolExecutorè controllato da due parametri, la dimensione del pool principale e la dimensione massima del pool . Se ci sono thread di dimensione inferiore al core pool attualmente attivi e arriva un nuovo lavoro, l'esecutore creerà un nuovo thread e lo eseguirà immediatamente. Se sono in esecuzione almeno thread della dimensione del pool di base, tenterà di mettere in coda il lavoro e attendere fino a quando non sarà disponibile un thread inattivo (cioè fino al completamento di un altro lavoro). Se non è possibile mettere in coda il lavoro (la coda può avere una capacità massima), creerà un nuovo thread (fino alla dimensione massima del pool di thread) per l'esecuzione dei lavori. I thread inattivi non core possono eventualmente essere rimossi secondo un parametro di timeout keep-alive.

Prima di Android 1.6, la dimensione del pool principale era 1 e la dimensione massima del pool 10. Poiché Android 1.6, la dimensione del pool principale è 5 e la dimensione massima del pool è 128. La dimensione della coda è 10 in entrambi i casi. Il timeout keep-alive era di 10 secondi prima della 2.3 e di 1 secondo da allora.

Con tutto questo in mente, ora diventa chiaro perché AsyncTaskapparirà solo per eseguire 5/6 delle tue attività. La sesta attività viene messa in coda fino al completamento di una delle altre attività. Questo è un ottimo motivo per cui non dovresti usare AsyncTask per operazioni di lunga durata: impedirà l'esecuzione di altri AsyncTask.

Per completezza, se hai ripetuto l'esercizio con più di 6 attività (es. 30), vedrai che ne entreranno più di 6 doInBackgroundman mano che la coda si riempirà e l'esecutore verrà spinto a creare più thread di lavoro. Se hai mantenuto l'attività di lunga durata, dovresti vedere che 20/30 diventa attivo, con 10 ancora in coda.


2
"Questo è un ottimo motivo per cui non dovresti usare AsyncTasks per operazioni di lunga durata" Qual è il tuo consiglio per questo scenario? Generare manualmente un nuovo thread o creare il proprio servizio di esecutore?
user123321

2
Gli esecutori sono fondamentalmente un'astrazione sopra i thread, alleviando la necessità di scrivere codice complesso per gestirli. Disaccoppia le tue attività da come dovrebbero essere eseguite. Se il tuo codice dipende solo da un esecutore, allora è facile cambiare in modo trasparente il numero di thread utilizzati, ecc. Non riesco davvero a pensare a una buona ragione per creare thread da solo, poiché anche per attività semplici, la quantità di lavoro un Executor è lo stesso, se non di meno.
antonyt

37
Tieni presente che da Android 3.0 e versioni successive, il numero predefinito di AsyncTask simultanei è stato ridotto a 1. Ulteriori informazioni: developer.android.com/reference/android/os/…
Kieran

Wow, grazie mille per l'ottima risposta. Finalmente ho una spiegazione del motivo per cui il mio codice non funziona in modo così sporadico e misterioso.
Chris Knight

@antonyt, Un altro dubbio, AsyncTasks annullati, conterà per il numero di AsyncTask? vale a dire, contato in core pool sizeo maximum pool size?
nmxprime

9

@antonyt ha la risposta giusta, ma se stai cercando una soluzione semplice, puoi controllare Needle.

Con esso puoi definire una dimensione del pool di thread personalizzato e, a differenza AsyncTask, funziona allo stesso modo su tutte le versioni di Android. Con esso puoi dire cose come:

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);
   }
});

o cose come

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

Può fare anche molto di più. Dai un'occhiata su GitHub .


l'ultimo commit
risale a

5

Aggiornamento : dall'API 19 la dimensione del pool di thread core è stata modificata per riflettere il numero di CPU sul dispositivo, con un minimo di 2 e un massimo di 4 all'inizio, mentre cresceva fino a un massimo di CPU * 2 +1 - Riferimento

// 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;

Si noti inoltre che mentre l'esecutore predefinito di AsyncTask è seriale (esegue un'attività alla volta e nell'ordine in cui arrivano), con il metodo

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

puoi fornire un esecutore per eseguire le tue attività. È possibile fornire THREAD_POOL_EXECUTOR l'esecutore sotto il cofano ma senza serializzazione delle attività, oppure è anche possibile creare il proprio Executor e fornirlo qui. Tuttavia, nota attentamente l'avvertimento nei Javadoc.

Avviso: consentire l'esecuzione di più attività in parallelo da un pool di thread in genere non è ciò che si desidera, poiché l'ordine delle loro operazioni non è definito. Ad esempio, se queste attività vengono utilizzate per modificare uno stato comune (come la scrittura di un file a causa del clic di un pulsante), non ci sono garanzie sull'ordine delle modifiche. Senza un lavoro accurato è possibile in rari casi che la versione più recente dei dati venga sovrascritta da una più vecchia, con conseguenti oscure perdite di dati e problemi di stabilità. Tali modifiche vengono eseguite al meglio in serie; per garantire che tale lavoro sia serializzato indipendentemente dalla versione della piattaforma è possibile utilizzare questa funzione con SERIAL_EXECUTOR.

Un'altra cosa da notare è che sia il framework fornito dagli Executors THREAD_POOL_EXECUTOR che la sua versione seriale SERIAL_EXECUTOR (che è l'impostazione predefinita per AsyncTask) sono statici (costrutti a livello di classe) e quindi condivisi tra tutte le istanze di AsyncTask (s) nel processo dell'app.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.