Python 协 程 : rendimiento, greenlet, gevent, asyncio

Proceso

El proceso es la unidad más pequeña de asignación de recursos, con espacio de memoria independiente, que incluye información de registro, montón, pila, segmento de datos, segmento de código, memoria virtual, identificador de archivo, estado de E / S, información de señal, etc., la sobrecarga de conmutación de diferentes procesos es relativamente grande Si bien el proceso es relativamente independiente y estable, generalmente no se ve afectado por otros procesos

La comunicación entre procesos incluye tuberías, colas de mensajes, semáforos, memoria compartida, sockets, etc.

Hilo

El subproceso es la unidad más pequeña de programación del sistema, solo necesita guardar su propia pila, registrar información y otro contenido pequeño, un proceso debe tener al menos un subproceso, la sobrecarga de conmutación de diferentes subprocesos es mucho menor que el cambio del proceso, pero el subproceso no es independiente y lo suficientemente estable como para ser vulnerable Y el impacto de otros hilos

Debido a que diferentes subprocesos comparten la misma memoria, la comunicación entre subprocesos usa directamente la memoria compartida, que es utilizar variables definidas globalmente. Además, los diferentes subprocesos generalmente también necesitan lograr la sincronización y la exclusión mutua a través de bloqueos

Corutina

El sistema operativo programa tanto los procesos como los subprocesos. Aunque la sobrecarga de conmutación de subprocesos es menor que el proceso, si se cambia con frecuencia, seguirá afectando seriamente el rendimiento

El sistema operativo generalmente cambia en tres situaciones

  1. El programa dura más.
  2. Preemption de programas con mayor prioridad
  3. El programa esta bloqueado

En muchas aplicaciones de red, se aceptará una gran cantidad de solicitudes al mismo tiempo. El cálculo de estas solicitudes es muy pequeño. El tiempo principal se gasta en IO, y el más importante es el tiempo de IO de la red, que causa el bloqueo frecuente de IO y el cambio de subprocesos, lo que afecta seriamente Rendimiento

La rutina es resolver el problema de rendimiento del programa con IO como la sobrecarga principal en escenarios de alta concurrencia

Puede ejecutar múltiples corutinas en un hilo. Cuando una corutina llama a un comando que requiere bloqueo de E / S, utilizará E / S asíncrona para evitar que el sistema operativo cambie y luego continúe ejecutando otra rutina. Implementado dentro de un hilo, la sobrecarga de conmutación es muy pequeña, el rendimiento mejorará considerablemente

Tenga en cuenta que la rutina es más obvia en el caso de una gran cantidad de concurrencia de E / S, porque solo en este caso podemos garantizar que la E / S asíncrona esté lista para ejecutarse en cualquier momento. Si la cantidad de E / S es pequeña, como una solicitud en 10 minutos, entonces Después de realizar una operación asincrónica, aún debe esperar a que el IO asincrónico esté listo, lo que seguirá causando el cambio de subprocesos

Tenga en cuenta que la rutina solo cambiará en un caso: IO call

Esta función debe ser implementada por el marco del programa, que es transparente para el sistema operativo y transparente para el programa de aplicación. Carga de trabajo de desarrollo

En el lenguaje Go, esta función es nativa. El lenguaje Go mismo implementa esta función y la admite
a nivel de sintaxis. En el lenguaje Python, esta función es compatible con el paquete gevent.

Lo siguiente habla principalmente sobre las corutinas de Python

rendimiento

El rendimiento es para uso del generador, como el siguiente código

def f(max): 
    n = 1
    while n <= max: 
        yield n*n
        n = n + 1
        
for i in f(5):
    print(i)

Si no usa el rendimiento, entonces la función f necesita devolver una lista, si max es muy grande, entonces necesita crear una memoria grande para poner esta lista, y después de usar el rendimiento, la función se considera como un iterador, f (5) Devuelve un iterador. Activa el iterador cada vez que la instrucción for toma un valor. Cuando el iterador ejecuta el comando de rendimiento, devuelve n * ny detiene la ejecución hasta la próxima vez que se toma el valor for. El iterador comienza desde n = n + 1 Continúe la ejecución para que no importa cuán grande sea el máximo, el uso de la memoria es constante

Dar otro ejemplo

def f():
    n = 1
    print("f function with yield inside")
    while True:
        msg = yield n
        print("msg: ", msg)
        n = n + 1

iter = f()
print("before invoke next")
print("receive: ", next(iter))
print("after invoke next")
print("receive: ", next(iter))

Lo que se devuelve es

before invoke next
f function with yield inside
('receive: ', 1)
after invoke next
('msg: ', None)
('receive: ', 2)

Se puede ver que cuando se llama a iter = f (), no se imprime ninguna información, es decir, la función f () no se ejecuta realmente, pero se devuelve un iterador, cuando se ejecuta la siguiente función (iter) (la siguiente es una función incorporada de Python) , La función f () se ejecuta, y aquí solo se ejecuta hasta obtener n, y luego deja de ejecutarse y devuelve n como resultado (aquí incluso no se ejecuta la asignación de msg, que se discutirá más adelante), cuando la próxima función siguiente, Continuará ejecutándose desde la asignación de msg hasta que alcance el rendimiento nuevamente. Si el iterador se ha ejecutado, la siguiente función informará una excepción StopIteration

Continuar con el siguiente ejemplo

def f():
    n = 1
    print("f function with yield inside")
    while True:
        msg = yield n
        print("msg: ", msg)
        n = n + 1

iter = f()
print("before invoke next")
print("receive: ", next(iter))
print("after invoke next")
print("receive: ", iter.send("from outside"))

Aquí, el segundo siguiente se reemplaza con la función de envío que llama al iterador
devuelve

before invoke next
f function with yield inside
('receive: ', 1)
after invoke next
('msg: ', 'from outside')
('receive: ', 2)

La única diferencia con el ejemplo anterior es que el mensaje impreso no es Ninguno, sino el parámetro de la función de envío. Al igual que el siguiente, la función de envío activa el iterador para continuar la ejecución, pero al mismo tiempo el parámetro se asigna al mensaje como resultado de la declaración de rendimiento.

Los siguientes usos rinden para simular corutinas

def f_0():
    n = 5
    while n >= 0:
        print('[f_0] ' + str(n))
        yield
		n = n - 1

def f_1():
    m = 3
    while m >= 0:
        print('[f_1] ' + str(m))
        yield
		m = m - 1

iter_list = [f_0(), f_1()]
while True:
    for it in iter_list:
	    try:
			next(it)
		except:
			iter_list.remove(it)
	
	if len(iter_list) == 0:
		break

El resultado es

[f_0] 5
[f_1] 3
[f_0] 4
[f_1] 2
[f_0] 3
[f_1] 1
[f_0] 2
[f_1] 0
[f_0] 1
[f_0] 0

Se puede ver que la función de cambiar continuamente entre las dos funciones se realiza, pero el código es engorroso de escribir

vireillo

greenlet es una biblioteca de extensión C que implementa corutinas nativas en la parte inferior

from greenlet import greenlet

def f_0():
    n = 5
    while n >= 0:
        print('[f_0] ' + str(n))
        parent_greenlet.switch()
        n = n - 1

def f_1():
    m = 3
    while m >= 0:
        print('[f_1] ' + str(m))
        parent_greenlet.switch()
        m = m - 1

def parent():
    while True:
        for task in greenlet_list:
            task.switch()
            if task.dead:
                greenlet_list.remove(task)
        if len(greenlet_list) == 0:
            break

parent_greenlet = greenlet(parent)
greenlet_list = [greenlet(f_0, parent_greenlet), greenlet(f_1, parent_greenlet)]
parent_greenlet.switch()

Volver

[f_0] 5
[f_1] 3
[f_0] 4
[f_1] 2
[f_0] 3
[f_1] 1
[f_0] 2
[f_1] 0
[f_0] 1
[f_0] 0

El interruptor también se puede pasar por valor, que se pasará al parámetro de función de acuerdo con el estado de ejecución del programa, o al retorno del interruptor

def test1(x, y):
    z = gr2.switch(x+y)
    print(z)

def test2(u):
    print(u)
    gr1.switch(42)
    print "end"

gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch("hello", " world")

Volver

hello world
42

Se puede ver que el final no se imprime porque el principal no está especificado. De manera predeterminada, un extremo vuelve a main y el otro no se ejecutará. Si se especifica el primario, se devolverá el principal después del final

traficado

Greenlet también es más complicado de escribir, y Greenlet solo implementa corutinas, pero no implementa la función de capturar operaciones de E / S y conmutación. De hecho, los cálculos generales no requieren el cambio de corutinas, y el rendimiento no tiene efecto, solo en operaciones de E / S muy concurrentes. Cuando el programa se puede cambiar, su rendimiento mejorará considerablemente

gevent se basa en greenlet y utiliza muchas medidas de optimización, incluido el mecanismo de supervisión de eventos epoll de linux para mejorar el rendimiento de las altas E / S concurrentes. Por ejemplo, cuando un programa greenlet necesita realizar operaciones de E / S de red, se registra como supervisión y conmutación asincrónicas Vaya a otros programas de greenlet, espere a que se complete IO, y luego vuelva a continuar para continuar la ejecución cuando sea apropiado, de modo que cuando el IO sea muy alto, pueda mantener el programa ejecutándose, en lugar de perder tiempo esperando IO, mientras evita el enhebrado Cambio por encima

import gevent

def f_0(param):
    n = param
    while n >= 0:
        print('[f_0] ' + str(n))
        gevent.sleep(0.1)
        n = n - 1

def f_1(param):
    m = param
    while m >= 0:
        print('[f_1] ' + str(m))
        gevent.sleep(0.1)
        m = m - 1

g1 = gevent.spawn(f_0, 5)
g2 = gevent.spawn(f_1, 3)
gevent.joinall([g1, g2])

Volver

[f_0] 5
[f_1] 3
[f_0] 4
[f_1] 2
[f_0] 3
[f_1] 1
[f_0] 2
[f_1] 0
[f_0] 1
[f_0] 0

Se puede ver que el código es conciso y claro. En comparación con el programa normal, es reemplazar time.sleep () con gevent.sleep (). Es gevent puede hacer un cambio de rutina donde necesita bloquearse.

En realidad puede ser más simple

import time
import gevent

from gevent import monkey

monkey.patch_all()

def f_0(param):
    n = param
    while n >= 0:
        print('[f_0] ' + str(n))
        time.sleep(0.1)
        n = n - 1

def f_1(param):
    m = param
    while m >= 0:
        print('[f_1] ' + str(m))
        time.sleep(0.1)
        m = m - 1

g1 = gevent.spawn(f_0, 5)
g2 = gevent.spawn(f_1, 3)
gevent.joinall([g1, g2])

La aplicación de parches con monkey.patch_all () puede interceptar una gran cantidad de operaciones de E / S, como tiempo de espera, solicitud http, etc., ejecutarlas de forma asincrónica y cambiar las rutinas. Este enfoque permite que la función original se use directamente sin modificación. Para los desarrolladores, las rutinas son transparentes, no es necesario modificar el código específicamente, solo déjelo en paz para que lo cuide

asyncio

Python 3.6 presentó oficialmente la biblioteca asyncio como la biblioteca estándar de Python

Las más importantes son las palabras clave asíncronas y aguardan

async se usa para declarar una función como asincrónica y se puede suspender

await se usa para declarar que el programa está suspendido. Await solo puede seguirlo un programa asincrónico o un objeto con el atributo __await__

import asyncio
import aiohttp

async def f_0(param):
    n = param
    while n >= 0:
        print('[f_0] ' + str(n))
        await asyncio.sleep(0.1)
        n = n - 1

async def f_1(param):
    m = param
    while m >= 0:
        print('[f_1] ' + str(m))
        await asyncio.sleep(0.1)
        m = m - 1

loop = asyncio.get_event_loop()

tasks = [
    f_0(5),
    f_1(3)
]

loop.run_until_complete(asyncio.wait(tasks))
loop.close()

Volver

[f_0] 5
[f_1] 3
[f_0] 4
[f_1] 2
[f_0] 3
[f_1] 1
[f_0] 2
[f_1] 0
[f_0] 1
[f_0] 0

Otro ejemplo

import asyncio
import aiohttp

async def request(session, url):
    async with session.get(url) as response:
        return await response.read()

async def fetch(url):
    await asyncio.sleep(1)
    async with aiohttp.ClientSession() as session:
        html = await request(session, url)
        print(html)

url_list = [
    "http://www.qq.com",
    "http://www.jianshu.com",
    "http://www.cnblogs.com"
]

tasks = [fetch(url) for url in url_list]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

Puede ver que necesita agregar asíncrono para admitir llamadas asíncronas, y use wait para especificar el lugar donde está suspendido.
Si el código especificado por await no puede suspenderse, se producirá un error
y deberá usarse un método o clase asíncrono específico.

En comparación, gevent puede ser transparente para el programa.
Un programa síncrono normal se puede lograr de forma asincrónica a través de gevent sin ninguna modificación.

Sin embargo, gevent usa el paquete tripartito, y asyncio es la biblioteca estándar de Python, que proporciona soporte a nivel de sintaxis.



Supongo que te gusta

Origin www.cnblogs.com/moonlight-lin/p/12732813.html
Recomendado
Clasificación