Cómo crear un administrador para varios subprocesos

En la guía anterior, que se explica Cómo especificar código para ejecutarlo en un subproceso, se muestra cómo definir una tarea que se ejecuta en un subproceso distinto. Si solo quieres ejecutar la tarea una única vez, es posible que no necesites realizar pasos adicionales. Si quieres ejecutarla varias veces en diferentes conjuntos de datos, pero solo necesitas que se realice una ejecución por vez, con un IntentService bastará. Si quieres ejecutar tareas automáticamente a medida que se habilitan los recursos, o bien permitir que varias tareas se ejecuten a la vez (o ambas acciones), deberás proporcionar una colección de subprocesos administrada. Para ello, usa una instancia de ThreadPoolExecutor, que ejecuta una tarea desde una cola cuando se libera un subproceso en el conjunto. Para ejecutar una tarea, solo tienes que agregarla a la cola.

Un conjunto de subprocesos puede ejecutar varias instancias paralelas de una tarea, por lo que deberías asegurarte de que tu código pueda usarse en subprocesos de forma segura. Encierra las variables a las que se puede acceder desde varios subprocesos en un bloque synchronized. Este enfoque evitará que un subproceso lea la variable cuando otro subproceso esté escribiendo en ella. Por lo general, esta situación se produce con las variables estáticas, pero también en cualquier objeto en el que se cree una sola instancia. Para obtener más información sobre este tema, consulta la siguiente guía: Descripción general de procesos y subprocesos.

Cómo definir la clase del conjunto de subprocesos

Crea instancias de ThreadPoolExecutor en su propia clase y haz lo siguiente:

Usa variables estáticas para los conjuntos de subprocesos
Es posible que solo quieras una única instancia de un conjunto de subprocesos en tu app, de manera que tengas un único punto de control de los recursos de red o CPU restringidos. Si tienes diferentes tipos de objetos Runnable, es posible que quieras tener un conjunto de subprocesos para cada uno, pero cada uno de estos puede ser una sola instancia. Por ejemplo, puedes agregarlo como parte de tus declaraciones de campo globales (para Kotlin puedes crear un objeto):

Kotlin

    // Creates a single static instance of PhotoManager
    object PhotoManager {
        ...
    }
    

Java

    public class PhotoManager {
        ...
        static  {
            ...
            // Creates a single static instance of PhotoManager
            sInstance = new PhotoManager();
        }
        ...
    
Usa un constructor privado
Hacer que el constructor sea privado garantiza que sea un singleton, lo que significa que no tienes que encerrar el acceso a la clase en un bloque synchronized (para Kotlin no es necesario tener un constructor privado porque este se define como un objeto, por lo que solo se inicializará una vez):

Kotlin

    object PhotoManager {
        ...
    }
    

Java

    public class PhotoManager {
        ...
        /**
         * Constructs the work queues and thread pools used to download
         * and decode images. Because the constructor is marked private,
         * it's unavailable to other classes, even in the same package.
         */
        private PhotoManager() {
        ...
        }
    
Para iniciar tus tareas, llama a los métodos en la clase del conjunto de subprocesos.
Define un método en la clase del conjunto de subprocesos que agrega una tarea a la cola del conjunto de subprocesos. Por ejemplo:

Kotlin

    object PhotoManager {
        ...
        fun startDownload(imageView: PhotoView, downloadTask: DownloadTask, cacheFlag: Boolean) =
                decodeThreadPool.execute(downloadTask.getHTTPDownloadRunnable())
        ...
    }
    

Java

    public class PhotoManager {
        ...
        // Called by the PhotoView to get a photo
        static public PhotoTask startDownload(
            PhotoView imageView,
            DownloadTask downloadTask,
            boolean cacheFlag) {
            ...
            // Adds a download task to the thread pool for execution
            sInstance.
                    downloadThreadPool.
                    execute(downloadTask.getHTTPDownloadRunnable());
            ...
        }
    
Crea una instancia de Handler en el constructor y adjúntala al subproceso de IU de tu app.
Un Handler permite a tu app llamar de forma segura a los métodos de los objetos de IU, como los objetos View. La mayoría de los objetos de IU solo pueden modificarse de forma segura desde el subproceso de IU. Este enfoque se describe con más detalle en la lección Cómo comunicarse con el subproceso de IU. Por ejemplo:

Kotlin

    object PhotoManager {
        ...
        private val handler = object : Handler(Looper.getMainLooper()) {

            /*
             * handleMessage() defines the operations to perform when
             * the Handler receives a new Message to process.
             */
            override fun handleMessage(msg: Message?) {
                ...
            }
            ...
        }
    }
    

Java

        private PhotoManager() {
        ...
            // Defines a Handler object that's attached to the UI thread
            handler = new Handler(Looper.getMainLooper()) {
                /*
                 * handleMessage() defines the operations to perform when
                 * the Handler receives a new Message to process.
                 */
                @Override
                public void handleMessage(Message inputMessage) {
                    ...
                }
            ...
            }
        }
    

Cómo determinar los parámetros del conjunto de subprocesos

Una vez que tengas la estructura de clase completa, puedes comenzar a definir el conjunto de subprocesos. Para crear una instancia de un objeto ThreadPoolExecutor, necesitas los siguientes valores:

Tamaño inicial y tamaño máximo del conjunto
Corresponde a la cantidad inicial de subprocesos para asignar al conjunto y la cantidad máxima permitida. La cantidad de subprocesos que puede tener en un conjunto de subprocesos depende principalmente de cuántos núcleos disponibles hay en tu dispositivo. Este valor se obtiene del entorno del sistema:

Kotlin

    object PhotoManager {
        ...
        /*
         * Gets the number of available cores
         * (not always the same as the maximum number of cores)
         */
        private val NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors()
    }
    

Java

    public class PhotoManager {
    ...
        /*
         * Gets the number of available cores
         * (not always the same as the maximum number of cores)
         */
        private static int NUMBER_OF_CORES =
                Runtime.getRuntime().availableProcessors();
    }
    
Es posible que este número no refleje la cantidad de núcleos físicos; algunos dispositivos tienen CPU que desactivan uno o varios núcleos según la carga del sistema. En el caso de estos dispositivos, availableProcessors() muestra la cantidad de núcleos activos, que puede ser menor que la cantidad total de núcleos.
Tiempo de mantenimiento de conexión y unidad de tiempo
Corresponde al tiempo durante el cual el subproceso se mantendrá inactivo antes de cerrarse. La duración se interpreta por el valor de unidad de tiempo, que es una de las constantes definidas en TimeUnit.
Una cola de tareas
Corresponde a la cola de entrada desde la que ThreadPoolExecutor toma los objetos Runnable. Para iniciar código en un subproceso, un administrador de conjuntos de subprocesos toma un objeto Runnable de una cola de "primero en entrar, primero en salir" y lo adjunta al subproceso. Este objeto de cola se proporciona cuando creas el conjunto de subprocesos por medio de cualquier clase de cola que implementa la interfaz BlockingQueue. Para cumplir con los requisitos de tu app, puedes elegir entre las implementaciones de cola disponibles. Si quieres obtener más información sobre estas, consulta la descripción general de la clase de ThreadPoolExecutor. En este ejemplo, se usa la clase LinkedBlockingQueue:

Kotlin

    object PhotoManager {
        ...
        // Instantiates the queue of Runnables as a LinkedBlockingQueue
        private val decodeWorkQueue: BlockingQueue<Runnable> = LinkedBlockingQueue<Runnable>()
        ...
    }
    

Java

    public class PhotoManager {
        ...
        private PhotoManager() {
            ...
            // A queue of Runnables
            private final BlockingQueue<Runnable> decodeWorkQueue;
            ...
            // Instantiates the queue of Runnables as a LinkedBlockingQueue
            decodeWorkQueue = new LinkedBlockingQueue<Runnable>();
            ...
        }
        ...
    }
    

Cómo crear un conjunto de subprocesos

Si quieres crear un conjunto de subprocesos, llama a ThreadPoolExecutor() a fin de crear una instancia de un administrador de conjuntos de subprocesos. De esta manera, se crea y administra un grupo de subprocesos restringido. Como el tamaño inicial y el tamaño máximo del conjunto son iguales, ThreadPoolExecutor crea todos los objetos del subproceso cuando se crean instancias de este. Por ejemplo:

Kotlin

    object PhotoManager {
        ...
        // Sets the amount of time an idle thread waits before terminating
        private const val KEEP_ALIVE_TIME = 1L
        // Sets the Time Unit to seconds
        private val KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS
        // Creates a thread pool manager
        private val decodeThreadPool: ThreadPoolExecutor = ThreadPoolExecutor(
                NUMBER_OF_CORES,       // Initial pool size
                NUMBER_OF_CORES,       // Max pool size
                KEEP_ALIVE_TIME,
                KEEP_ALIVE_TIME_UNIT,
                decodeWorkQueue
        )
    

Java

        private PhotoManager() {
            ...
            // Sets the amount of time an idle thread waits before terminating
            private static final int KEEP_ALIVE_TIME = 1;
            // Sets the Time Unit to seconds
            private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
            // Creates a thread pool manager
            decodeThreadPool = new ThreadPoolExecutor(
                    NUMBER_OF_CORES,       // Initial pool size
                    NUMBER_OF_CORES,       // Max pool size
                    KEEP_ALIVE_TIME,
                    KEEP_ALIVE_TIME_UNIT,
                    decodeWorkQueue);
        }
    

De manera alternativa, si no quieres administrar los detalles de la definición del tamaño del conjunto de subprocesos, puede que los métodos de fábrica de los Ejecutadores para crear un ejecutador de subprocesos único o el ejecutador de robo de tareas te resulten más convenientes.

Más información

Para obtener más información sobre las operaciones de varios subprocesos en Android, consulta la siguiente guía: Descripción general de procesos y subprocesos.

App de ejemplo

Si quieres probar los conceptos de esta guía, descarga ThreadSample.