jueves, 24 de marzo de 2011

Acerca de muestreos, módulo futures y ejecución en paralelo

Artículo original: Of polling, futures and parallel execution

Una de las mayores preocupaciones en la computación moderna es el ahorro de energía. Es muy importante en dispositivos portátiles (portátiles, tabletas, móviles). Una CPU moderna es capaz de entrar en un número variado de estados de ahorro de energía cuando está ociosa. Cuanto mayor tiempo se mantenga en estado ocioso, más profundo será el estado de ahorro de energía y menor será la energía consumida y por lo tanto, la duración de la batería será más larga en cada carga.

Los estados de ahorro de energía tienen un enemigo: los muestreos. Cuando una tarea activa la CPU periódicamente, incluso por algo tan trivial como leer una dirección de memoria para verificar si ha habido cambios potenciales, la CPU abandona el estado de ahorro de energía, activa a todas sus estructuras internas y solo volverá a entrar en este estado mucho después de haber terminado su labor causada por esa activación de poca importancia. Esto acaba con la vida de la batería. La propia Intel está preocupada por esto.

Python 3.2 viene con un nuevo módulo estándar para lanzar tareas concurrentes y esperar que terminen: el módulo concurrent.futures. Mientras hojeaba su código, noté que usaba muestreos en algunos de sus hilos y procesos trabajadores. Y digo «algunos de» ya que la implementación de ThreadPoolExecutor (basada en hilos) es diferente que la implementación de ProcessPoolExecutor (basada en procesos). El primero hace muestreos en cada uno de los hilos trabajadores, mientras que el último solo lo hace en un único hilo llamado el hilo de gestión de la cola, el cual se usa para comunicarse con el proceso que realiza el trabajo.

Los muestreos se usaron aquí para una sola cuestión: detectar cuando debería empezar el procedimiento de apagado. Otras tareas, como encolar objetos que puedan ser invocados o buscar los resultados de uno de esos objetos encolado previamente, usan objetos sincronizados de tipo cola. Estos objetos proceden bien del módulo threading o bien del multiprocessing, dependiendo de la implementación del ejecutor que se esté utilizando.

Así que se me ocurrió una solución simple: Sustituí el muestreo por un centinela, el centinela None ya integrado. Cuando una cola recibe None, un trabajador en espera se activa de forma natural y verifica si se debería apagar o no. En el ProcessPoolExecutor hay una pequeña complicación ya que necesitamos activar hasta N procesos trabajadores además del hilo de gestión de colas.

En mi parche inicial, todavía había un tiempo de espera de muestreo muy largo (10 minutos) de manera que los trabajadores pudieran activarse en algún momento. Este largo tiempo de espera existía por si acaso el código contenía bugs y no recibían una notificación de apagado cuando debían a través del ya mencionado centinela. Por curiosidad, me sumergí en el código fuente de multiprocessing y llegué a otra observación interesante: bajo Windows, multiprocessing.Queue.get() con un tiempo de espera no nulo y finito usaba ... muestreo (por lo cual abrí el asunto 11668). Utiliza un tipo de muestreo de alta frecuencia muy interesante, ya que se comienza con un tiempo de espera de un milisegundo el cual se incrementa en cada iteración.

No hace falta mencionar que aunque usara un tiempo de espera muy grande, haría mi parche inútil en Windows ya que la forma en que el tiempo de espera está implementado implicaría activaciones en cada milisegundo. Así que no me quedó más remedio y eliminar el enorme tiempo de muestreo. Mi último parche ni siquiera utiliza un tiempo de espera, y por lo tanto no debería causar activaciones periódicas, independientemente de la plataforma.

Echando un vistazo atrás, antes de Python 3.2, toda estructura para esperas en el módulo threading, y por lo tanto en muchas partes del módulo multiprocessing ya que él mismo utiliza hilos trabajadores para varias tareas, usaba muestreo. Esto se corrigió en el asunto 7316.

No hay comentarios:

Publicar un comentario