95 votes

Limites des threads d'AsyncTask Android ?

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 une base de données dans le téléphone. Pour toutes ces opérations (mises à jour, récupération de données dans la base de données, etc.), j'utilise des tâches asynchrones. Jusqu'à présent, je ne voyais pas pourquoi je ne devais pas les utiliser, mais récemment, j'ai constaté que si je faisais certaines opérations, certaines de mes tâches asynchrones s'arrêtaient tout simplement en pré-exécution et ne passaient pas à doInBackground. C'était 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 étrangement, j'obtiens le même comportement lorsque le nombre total de tâches asynchrones atteint 5, la 6ème s'arrête lors du pre-execute.

Est-ce qu'Android a une limite d'asyncTasks sur une activité/application ? Ou s'agit-il simplement d'un bug qu'il faut signaler ? Est-ce que quelqu'un a rencontré le même problème et a peut-être trouvé une solution de contournement ?

Voici le code :

Il suffit de créer 5 de ces fils 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 ensuite créer ce fil. Il entrera dans preExecute et se suspendra (il ne passera 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");
    }
}

207voto

antonyt Points 10649

Toutes les AsyncTasks sont contrôlées en interne par une méthode partagée (statique) ThreadPoolExecutor et un LinkedBlockingQueueue . Lorsque vous appelez execute sur une AsyncTask, le ThreadPoolExecutor l'exécutera lorsqu'il sera prêt dans le futur.

Le comportement "quand suis-je prêt ?" d'une ThreadPoolExecutor est contrôlée par deux paramètres, le taille du pool de base et le taille maximale de la piscine . S'il y a moins de threads de taille core pool actuellement actifs et qu'un nouveau travail arrive, l'exécuteur créera un nouveau thread et l'exécutera immédiatement. S'il y a au moins des threads de taille core pool en cours d'exécution, il essaiera de mettre le travail en file d'attente et attendra qu'un thread libre 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 l'exécution des travaux. Les threads inactifs non centraux peuvent éventuellement être déclassés en fonction d'un paramètre de délai de maintien en activité.

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 d'attente de la fonction "keep-alive" était de 10 secondes avant la version 2.3, et de 1 seconde depuis.

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

Pour être complet, si vous répétez votre exercice avec plus de 6 tâches (par exemple, 30), vous verrez que plus de 6 entreront doInBackground car la file d'attente sera pleine et l'exécuteur sera poussé à créer plus de threads de travail. Si vous continuez avec la tâche qui s'exécute depuis longtemps, vous devriez voir que 20/30 deviennent actifs, avec 10 toujours dans la file d'attente.

2 votes

"C'est une très bonne raison pour ne pas utiliser AsyncTasks pour les opérations de longue durée" Quelle est votre recommandation pour ce scénario ? Lancer un nouveau thread manuellement ou créer votre propre service d'exécution ?

2 votes

Les exécuteurs sont essentiellement une abstraction au-dessus des threads, ce qui évite d'avoir à écrire du code complexe pour les gérer. Ils dissocient vos tâches de la manière dont elles doivent être exécutées. Si votre code ne dépend que d'un exécuteur, il est facile de modifier de manière transparente le nombre de threads utilisés, etc. Je ne vois pas vraiment de bonne raison de créer des threads soi-même, puisque même pour des tâches simples, la quantité de travail avec un exécuteur est la même, voire moindre.

37 votes

Veuillez noter qu'à partir d'Android 3.0+, le nombre par défaut d'AsyncTasks simultanées a été réduit à 1. Plus d'informations : developer.Android.com/reference/Android/os/

10voto

Zsolt Safrany Points 1688

@antonyt a la bonne réponse mais si 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 aux AsyncTask il fonctionne sur todo Les versions Android sont les mêmes. Avec ça, 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
   }
}); 

Il peut faire encore beaucoup plus. Découvrez-le sur GitHub .

0 votes

Le dernier commit date de 5 ans :(

5voto

rnk Points 676

Mise à jour : Depuis l'API 19, la taille du pool de threads de base a été modifiée pour refléter le nombre de CPU sur le périphérique, avec un minimum de 2 et un maximum de 4 au départ, 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, alors que l'exécuteur par défaut d'AsyncTask est sériel (il exécute une tâche à la fois et dans l'ordre dans lequel elles arrivent), avec l'attribut 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 bien l'avertissement dans les Javadocs.

Avertissement : Permettre à plusieurs tâches de 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 sur un 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 une perte de données obscures et des problèmes de stabilité. De telles modifications sont mieux exécutées en série ; pour garantir que ce travail est sérialisé indépendamment de la version de la plate-forme, vous pouvez utiliser cette fonction avec SERIAL_EXECUTOR.

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X