Limites des threads Android AsyncTask?

95

Je développe une application dans laquelle je dois mettre à jour certaines informations chaque fois que l'utilisateur se connecte au système, j'utilise également la base de données du téléphone. Pour toutes ces opérations (mises à jour, récupération de données à partir de la base de données, etc.), j'utilise des tâches asynchrones. Comme jusqu'à présent, je n'ai pas vu pourquoi je ne devrais pas les utiliser, mais récemment, j'ai constaté que si je fais certaines opérations, certaines de mes tâches asynchrones s'arrêtent simplement lors de la pré-exécution et ne sautent pas à doInBackground. C'était juste trop étrange pour le laisser comme ça, alors j'ai développé une autre application simple juste pour vérifier ce qui ne va pas. Et assez étrange, j'obtiens le même comportement lorsque le nombre total de tâches asynchrones atteint 5, le 6ème s'arrête lors de la pré-exécution.

Android a-t-il une limite d'AsyncTasks sur l'activité / l'application? Ou est-ce juste un bug et il devrait être signalé? Quelqu'un a-t-il rencontré le même problème et a peut-être trouvé une solution de contournement?

Voici le code:

Créez simplement 5 de ces threads pour travailler en arrière-plan:

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

Et puis créez ce fil. Il entrera preExecute et se bloquera (il n'ira pas à 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
la source

Réponses:

207

Toutes les AsyncTasks sont contrôlées en interne par un ThreadPoolExecutor partagé (statique) et un LinkedBlockingQueue . Lorsque vous appelez executeune AsyncTask, l ' ThreadPoolExecutorl'exécutera lorsqu'elle sera prête dans le futur.

Le "quand suis-je prêt?" le comportement de a ThreadPoolExecutorest contrôlé par deux paramètres, la taille du pool central et la taille maximale du pool . S'il y a moins de threads de taille de pool de cœurs actuellement actifs et qu'un nouveau travail arrive, l'exécuteur crée un nouveau thread et l'exécute immédiatement. S'il y a au moins des threads de taille de pool de cœurs en cours d'exécution, il essaiera de mettre le travail en file d'attente et attendra qu'un thread inactif soit disponible (c'est-à-dire jusqu'à ce qu'un autre travail soit terminé). S'il n'est pas possible de mettre le travail en file d'attente (la file d'attente peut avoir une capacité maximale), il créera un nouveau thread (jusqu'à la taille maximale du pool de threads) pour que les travaux s'exécutent. Les threads inactifs non principaux peuvent éventuellement être mis hors service selon un paramètre de délai d'attente de maintien.

Avant Android 1.6, la taille du pool principal était de 1 et la taille maximale du pool était de 10. Depuis Android 1.6, la taille du pool principal est de 5 et la taille maximale du pool est de 128. La taille de la file d'attente est de 10 dans les deux cas. Le délai de maintien en vie était de 10 secondes avant 2,3 et 1 seconde depuis.

Avec tout cela à l'esprit, il devient maintenant clair pourquoi le AsyncTaskne semble exécuter que 5/6 de vos tâches. La 6ème tâche est mise en file d'attente jusqu'à ce que l'une des autres tâches soit terminée. C'est une très bonne raison pour laquelle vous ne devriez pas utiliser AsyncTasks pour des opérations de longue durée - cela empêchera d'autres AsyncTasks de s'exécuter.

Par souci d'exhaustivité, si vous répétez votre exercice avec plus de 6 tâches (par exemple 30), vous verrez que plus de 6 entreront doInBackgroundcar la file d'attente deviendra pleine et l'exécuteur est poussé à créer plus de threads de travail. Si vous continuez avec la tâche de longue durée, vous devriez voir que 20/30 deviennent actifs, avec 10 toujours dans la file d'attente.

Antonyt
la source
2
"C'est une très bonne raison pour laquelle vous ne devriez pas utiliser AsyncTasks pour des opérations de longue durée" Quelle est votre recommandation pour ce scénario? Créer un nouveau thread manuellement ou créer votre propre service d'exécuteur?
user123321
2
Les exécuteurs sont essentiellement une abstraction au-dessus des threads, ce qui évite d'avoir à écrire du code complexe pour les gérer. Il dissocie vos tâches de la façon dont elles doivent être exécutées. Si votre code ne dépend que d'un exécuteur, il est facile de changer de manière transparente le nombre de threads utilisés, etc. Je ne peux pas vraiment penser à une bonne raison de créer des threads vous-même, car même pour des tâches simples, la quantité de travail avec un exécuteur est le même, sinon moins.
antonyt
37
Veuillez noter qu'à partir d'Android 3.0+, le nombre par défaut d'AsyncTasks simultanés a été réduit à 1. Plus d'infos: developer.android.com/reference/android/os/...
Kieran
Wow, merci beaucoup pour une excellente réponse. Enfin, j'ai une explication sur les raisons pour lesquelles mon code échoue si sporadiquement et mystérieusement.
Chris Knight
@antonyt, Encore un doute, AsyncTasks annulé, cela comptera-t-il jusqu'au nombre d'AsyncTasks? c'est-à-dire, compté dans core pool sizeou maximum pool size?
nmxprime
9

@antonyt a la bonne réponse, mais au cas où vous cherchez une solution simple, vous pouvez consulter Needle.

Avec lui, vous pouvez définir une taille de pool de threads personnalisée et, contrairement à AsyncTask, cela fonctionne de la même manière sur toutes les versions d'Android. Avec lui, vous pouvez dire des choses comme:

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 des choses comme

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

Cela peut faire bien plus. Découvrez-le sur GitHub .

Zsolt Safrany
la source
le dernier commit
remonte à
5

Mise à jour : depuis l'API 19, la taille du pool de threads de base a été modifiée pour refléter le nombre de processeurs sur l'appareil, avec un minimum de 2 et un maximum de 4 au démarrage, tout en augmentant jusqu'à un maximum de CPU * 2 +1 - Référence

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

Notez également que si l'exécuteur par défaut d'AsyncTask est série (exécute une tâche à la fois et dans l'ordre dans lequel elles arrivent), avec la méthode

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

vous pouvez fournir un exécuteur pour exécuter vos tâches. Vous pouvez fournir le THREAD_POOL_EXECUTOR à l'exécuteur sous le capot, mais sans sérialisation des tâches, ou vous pouvez même créer votre propre exécuteur et le fournir ici. Cependant, notez attentivement l'avertissement dans les Javadocs.

Attention: autoriser plusieurs tâches à s'exécuter en parallèle à partir d'un pool de threads n'est généralement pas ce que l'on souhaite, car l'ordre de leur fonctionnement n'est pas défini. Par exemple, si ces tâches sont utilisées pour modifier un état en commun (comme l'écriture d'un fichier suite à un clic de bouton), il n'y a aucune garantie sur l'ordre des modifications. Sans un travail minutieux, il est possible dans de rares cas que la version la plus récente des données soit écrasée par une version plus ancienne, ce qui entraîne d'obscurs problèmes de perte de données et de stabilité. Ces changements sont mieux exécutés en série; pour garantir que ce travail est sérialisé quelle que soit la version de la plate-forme, vous pouvez utiliser cette fonction avec SERIAL_EXECUTOR.

Une autre chose à noter est que le framework fourni Executors THREAD_POOL_EXECUTOR et sa version série SERIAL_EXECUTOR (qui est par défaut pour AsyncTask) sont statiques (constructions au niveau de la classe) et donc partagés entre toutes les instances d'AsyncTask (s) à travers votre processus d'application.

rnk
la source