Estudio en profundidad de concurrencia en Python (2) ----- avanzado

Considere los siguientes requisitos:

Tenemos un directorio de registro, que está lleno de archivos de registro comprimidos de gzip.

El formato de cada archivo de registro es fijo, queremos extraer todos los hosts que han accedido al archivo robots.txt

1.1.1.1 ------------ [10 / junio / 2012: 00: 18: 50 - 0500] "GET /robots.txt ..." 200 71

2.1.1.3 ------------ [12 / junio / 2013: 00: 18: 50 - 0500] "GET /a.txt ..." 202 73

122.1.1.3 ------------ [12 / junio / 2013: 00: 18: 50 - 0500] "GET /robots.txt ..." 202 73

 

Sin usar concurrencia, escribiremos el siguiente código de programa:

import gzip
import glob
import io

def find_robots(filename):
    robots = set()
    with gzip.open(filename) as f:
        for line in io.TextIOWrapper(f, encoding='ascii'):
            fields = line.split()
            if fields[6] == '/robots.txt':
                robots.add(fields[0])
    return robots

def find_all_robots(logdir):
    files = glob.glob(logdir+'/*.log.gz')
    all_robots = set()
    for robots in map(find_robots, files):
        all_robots.update(robots)
    return all_robots

El programa anterior está escrito en el estilo de map-reduce.

Si desea reescribir el programa anterior para usar múltiples núcleos de CPU. Simplemente reemplace el mapa con una operación similar y deje que se ejecute en el grupo de procesos en la biblioteca concurrent.futures.

Aquí está el código ligeramente modificado:

def find_all_robots(logdir):
    files = glob.glob(logdir+'/*.log.gz')
    all_robots = set()
    with ProcessPoolExecutor as pool:
        for robots in pool.map(find_robots, files):
            all_robots.update(robots)
    return all_robots

El uso típico de ProcessPoolExecutor es el siguiente:

from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
    do work in parallel using pool

En la parte inferior, ProcessPoolExcutor usa N procesos independientes para iniciar el intérprete de Python, N es el número de CPU, o puede pasar ProcessPoolExecutor (N) a través del parámetro N. Hasta que se ejecute la última instrucción en el bloque with, ProcessPoolExecutor saldrá y esperará antes de salir Todas las tareas están completadas.

La tarea enviada al grupo de procesos debe tener la forma de una función. Hay 2 formas de enviar una tarea. Si desea procesar una comprensión de lista o una operación de mapa en paralelo, puede usar pool.map, o puede enviar manualmente una tarea con submit.

def work(x):
    result = '''
    '''
    return result

from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
    future_result = pool.submit(work)

    r = future_result.result()

Enviar manualmente la tarea devolverá un objeto futuro, puede obtener el resultado a través del método de resultado, pero el método de resultado se bloqueará hasta que el resultado regrese.

Para evitar que se bloquee, también puede instalar una función de finalización de procesamiento.

def when_done(r):
    print('Got:', r.result())

with ProcessPoolExecutor() as pool:
    future_result = pool.submit(work)
    future_result.add_done_callback(when_done)

Aunque el grupo de procesos es muy simple de usar, es necesario prestar atención a los siguientes puntos:

1. Esta tecnología de procesamiento paralelo solo es adecuada para situaciones en las que el problema puede descomponerse en partes separadas

2. Las tareas solo se pueden definir como funciones ordinarias para enviar, los métodos de instancia, los cierres u otros tipos de objetos invocables no admiten el procesamiento paralelo

3. Los parámetros y el valor de retorno de la función deben ser compatibles con la codificación pickle. La ejecución de la tarea se realiza en un proceso de intérprete separado, que requiere comunicación entre procesos. Por lo tanto, el intercambio de datos entre diferentes intérpretes debe ser serializado.

4. La función de trabajo presentada no debe mantener un estado persistente ni tener efectos secundarios.

5. En el entorno UNIX, el grupo de procesos se implementa a través de la llamada del sistema fork

6. Tenga mucho cuidado al combinar el grupo de procesos y el grupo de subprocesos, por lo general, debe iniciar el grupo de procesos antes de crear el grupo de subprocesos

 

Cómo eludir las restricciones de GIL.

En la implementación del lenguaje C del intérprete de Python, parte del código no es seguro para subprocesos, por lo que no se puede ejecutar de manera completamente simultánea. De hecho, el intérprete está protegido por algo llamado bloqueo global del intérprete (GIL), y solo un hilo de Python puede ejecutarse en cualquier momento. El impacto más obvio de GIL es que los programas Python de subprocesos múltiples no pueden aprovechar al máximo las CPU de varios núcleos (es decir, una aplicación computacionalmente intensiva que utiliza tecnología de subprocesos múltiples solo puede ejecutarse en una CPU)

Para comprender el GIL, debe saber cuándo Python lanza el GIL.

El intérprete liberará el GIL siempre que esté bloqueado en espera de una operación de E / S. Para los subprocesos intensivos de CPU que nunca realizan operaciones de bloqueo, el intérprete de Python lanzará el GIL después de ejecutar un cierto número de bytecodes, para que otros subprocesos tengan la oportunidad de ejecutarse. Sin embargo, el módulo de extensión de lenguaje C es diferente. Cuando se llama a la función C, el GIL se bloqueará hasta que regrese.

Dado que el intérprete no controla el código C, no se ejecutará ningún código de bytes de Python durante este período, por lo que el intérprete no puede liberar el GIL.

HABLE TANTO. Para sortear las restricciones de GIL, generalmente hay dos estrategias:

1. Si está programando completamente en Python, use el módulo de multiprocesamiento para crear un grupo de procesos y úselo como coprocesador.

2. Concéntrese en la extensión del lenguaje C. La idea principal es transferir las tareas de computación intensiva al código C, hacerlo independiente de Python y liberar el GIL en el código C. Esto se logra insertando macros especiales en el código C:

#include "Python.h"

PyObject *pyfunc(PyObject *self, PyObject *args)
{
    ...
    Py_BEGIN_ALLOW_THREADS
    // Threaded C code
    ...
    Py_END_ALLOW_THREADS
    ...
}

Si usa la biblioteca de cyptes o Cython para acceder al código C, entonces ctypes liberará automáticamente el GIL sin nuestra intervención.

 

 

230 artículos originales publicados · 160 alabanzas · 820,000 visitas

Supongo que te gusta

Origin blog.csdn.net/happyAnger6/article/details/104483242
Recomendado
Clasificación