¿Límites de subprocesos de Android AsyncTask?

95

Estoy desarrollando una aplicación en la que necesito actualizar cierta información cada vez que el usuario inicia sesión en el sistema, también uso la base de datos en el teléfono. Para todas esas operaciones (actualizaciones, recuperación de datos de db, etc.) utilizo tareas asíncronas. Como hasta ahora, no vi por qué no debería usarlos, pero recientemente experimenté que si hago algunas operaciones, algunas de mis tareas asíncronas simplemente se detienen en la ejecución previa y no saltan a doInBackground. Eso fue demasiado extraño para dejarlo así, así que desarrollé otra aplicación simple solo para verificar qué estaba mal. Y por extraño que parezca, obtengo el mismo comportamiento cuando el recuento de tareas asíncronas totales llega a 5, el sexto se detiene en la ejecución previa.

¿Android tiene un límite de asyncTasks en Actividad / Aplicación? ¿O es solo un error y debería informarse? ¿Alguien experimentó el mismo problema y tal vez encontró una solución?

Aquí está el código:

Simplemente cree 5 de esos hilos para trabajar en 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");
    }
}

Y luego crea este hilo. Entrará en preExecute y se colgará (no irá 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");
    }
}
Mindaugas Svirskas
fuente

Respuestas:

207

Todas las AsyncTasks están controladas internamente por un ThreadPoolExecutor compartido (estático) y un LinkedBlockingQueue . Cuando llame executea una AsyncTask, la ThreadPoolExecutorejecutará cuando esté lista en el futuro.

El '¿cuándo estoy listo?' El comportamiento de a ThreadPoolExecutorestá controlado por dos parámetros, el tamaño del grupo principal y el tamaño máximo del grupo . Si hay menos subprocesos del tamaño del grupo principal actualmente activos y entra un nuevo trabajo, el ejecutor creará un nuevo subproceso y lo ejecutará inmediatamente. Si hay al menos subprocesos del tamaño del grupo central en ejecución, intentará poner en cola el trabajo y esperará hasta que haya un subproceso inactivo disponible (es decir, hasta que se complete otro trabajo). Si no es posible poner en cola el trabajo (la cola puede tener una capacidad máxima), se creará un nuevo subproceso (subprocesos del tamaño de grupo máximo) para que se ejecuten los trabajos. Los subprocesos inactivos que no son del núcleo pueden eventualmente ser retirados de acuerdo con un parámetro de tiempo de espera de mantener vivo.

Antes de Android 1.6, el tamaño del grupo principal era 1 y el tamaño máximo del grupo era 10. Desde Android 1.6, el tamaño del grupo principal es 5 y el tamaño máximo del grupo es 128. El tamaño de la cola es 10 en ambos casos. El tiempo de espera para mantener vivo fue 10 segundos antes de 2.3 y 1 segundo desde entonces.

Con todo esto en mente, ahora queda claro por qué AsyncTasksolo aparecerá para ejecutar 5/6 de sus tareas. La sexta tarea se pone en cola hasta que se complete una de las otras tareas. Esta es una muy buena razón por la que no debe usar AsyncTasks para operaciones de larga duración: evitará que otras AsyncTasks se ejecuten.

Para completar, si repitió su ejercicio con más de 6 tareas (por ejemplo, 30), verá que entrarán más de 6 doInBackgroundya que la cola se llenará y el ejecutor será empujado para crear más subprocesos de trabajo. Si siguió con la tarea de ejecución prolongada, debería ver que 20/30 se activan, con 10 todavía en la cola.

antonyt
fuente
2
"Ésta es una muy buena razón por la que no debería utilizar AsyncTasks para operaciones de larga duración" ¿Cuál es su recomendación para este escenario? ¿Generar un nuevo hilo manualmente o crear su propio servicio ejecutor?
user123321
2
Los ejecutores son básicamente una abstracción en la parte superior de los subprocesos, lo que alivia la necesidad de escribir código complejo para administrarlos. Desacopla sus tareas de cómo deben ejecutarse. Si su código solo depende de un ejecutor, entonces es fácil cambiar de forma transparente cuántos subprocesos se utilizan, etc. Realmente no puedo pensar en una buena razón para crear subprocesos usted mismo, ya que incluso para tareas simples, la cantidad de trabajo con un Ejecutor es lo mismo, si no menos.
antonyt
37
Tenga en cuenta que desde Android 3.0+, el número predeterminado de AsyncTasks concurrentes se ha reducido a 1. Más información: developer.android.com/reference/android/os/…
Kieran
Vaya, muchas gracias por una gran respuesta. Finalmente, tengo una explicación de por qué mi código falla de manera tan esporádica y misteriosa.
Chris Knight
@antonyt, Una duda más, AsyncTasks canceladas, ¿contará para el número de AsyncTasks? es decir, contados en core pool sizeo maximum pool size?
nmxprime
9

@antonyt tiene la respuesta correcta, pero en caso de que esté buscando una solución simple, puede consultar Needle.

Con él, puede definir un tamaño de grupo de subprocesos personalizado y, a diferencia AsyncTask, funciona en todas las versiones de Android por igual. Con él puedes decir cosas 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);
   }
});

o cosas como

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

Puede hacer mucho más. Compruébalo en GitHub .

Zsolt Safrany
fuente
la última confirmación fue hace 5 años :(
LukaszTaraszka
5

Actualización : desde API 19, el tamaño del grupo de subprocesos centrales se cambió para reflejar la cantidad de CPU en el dispositivo, con un mínimo de 2 y un máximo de 4 al inicio, mientras crece hasta un máximo de CPU * 2 +1 - Referencia

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

También tenga en cuenta que si bien el ejecutor predeterminado de AsyncTask es serial (ejecuta una tarea a la vez y en el orden en que llegan), con el método

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

puede proporcionar un ejecutor para ejecutar sus tareas. Puede proporcionar THREAD_POOL_EXECUTOR el ejecutor bajo el capó pero sin serialización de tareas, o incluso puede crear su propio Ejecutor y proporcionarlo aquí. Sin embargo, tenga en cuenta la advertencia en los Javadocs.

Advertencia: Permitir que varias tareas se ejecuten en paralelo desde un grupo de subprocesos generalmente no es lo que uno desea, porque el orden de su operación no está definido. Por ejemplo, si estas tareas se utilizan para modificar cualquier estado en común (como escribir un archivo debido al clic de un botón), no hay garantías sobre el orden de las modificaciones. Sin un trabajo cuidadoso, en raras ocasiones es posible que la versión más reciente de los datos sea sobrescrita por una anterior, lo que genera problemas de estabilidad y pérdida de datos oscuros. Es mejor ejecutar estos cambios en serie; Para garantizar que dicho trabajo sea serializado independientemente de la versión de la plataforma, puede utilizar esta función con SERIAL_EXECUTOR.

Una cosa más a tener en cuenta es que tanto el marco proporcionado Executors THREAD_POOL_EXECUTOR como su versión serial SERIAL_EXECUTOR (que es predeterminada para AsyncTask) son estáticos (construcciones de nivel de clase) y, por lo tanto, se comparten en todas las instancias de AsyncTask en el proceso de su aplicación.

rnk
fuente