Понимание сопрограмм Python

Python

Из-за наличия GIL многопоточная производительность Python даже хуже, чем однопоточная.

GIL: Глобальная блокировка интерпретатора (англ. Global Interpreter Lock, сокращенно GIL) — это механизм, используемый интерпретаторами языков компьютерного программирования для синхронизации потоков, чтобы в каждый момент времени выполнялся только один поток. [1] Даже на многоядерных процессорах интерпретаторы, использующие GIL, позволяют одновременно выполнять только один поток.

Итак, есть такое понятие, как сопрограмма (Coroutine).

Coroutine: Coroutine, также известная как микронить, волокно, английское название Coroutine. Роль сопрограммы заключается в том, что при выполнении функции A она может быть прервана в любой момент для выполнения функции B, а затем прервана для продолжения выполнения функции A (которую можно свободно переключать). Но этот процесс не является вызовом функции (нет оператора вызова), весь процесс вроде бы многопоточный, но сопрограмма выполняется только одним потоком.

Так как программа активно управляет переключением, отсутствуют накладные расходы на переключение потоков, поэтому эффективность выполнения чрезвычайно высока. Он очень подходит для задач с интенсивным вводом-выводом, если он интенсивно использует процессор, рекомендуется метод multi-process + coroutine.

До Python 3.4 не было официальной поддержки сопрограмм, и были реализации некоторых сторонних библиотек, таких как gevent и Tornado. После версии 3.4 была встроена стандартная библиотека asyncio и официальная реализация функции сопрограммы.

Поддержка Python для сопрограмм реализована через Generator, а сопрограммы — это генераторы, которые следуют определенным правилам. Итак, прежде чем мы узнаем о сопрограммах, нам нужно узнать о генераторах.

Генератор

В основном мы обсуждаем здесьyieldиyield fromЭти два выражения, эти два выражения тесно связаны с реализацией сопрограммы.

  • Представлено в Python 2.5yieldвыражение см.PEP342
  • Добавлено в Python 3.3yield fromсинтаксис см.PEP380,

метод содержитyieldПосле выражения Python будет рассматривать его как объект генератора, а не как обычный метод.

yieldиспользование выражений

Давайте сначала посмотрим на конкретное использование этого выражения:

def test():
    print("generator start")
    n = 1
    while True:
        yield_expression_value = yield n
        print("yield_expression_value = %d" % yield_expression_value)
        n += 1


# ①创建generator对象
generator = test()
print(type(generator))

print("\n---------------\n")

# ②启动generator
next_result = generator.__next__()
print("next_result = %d" % next_result)

print("\n---------------\n")

# ③发送值给yield表达式
send_result = generator.send(666)
print("send_result = %d" % send_result)

Результаты:

<class 'generator'>

---------------

generator start
next_result = 1

---------------

yield_expression_value = 666
send_result = 2

Описание метода:

  • __next__()Метод: функция запускает или возобновляет выполнение генератора, что эквивалентно send(None)

  • send(value)Метод: роль заключается в отправке значения в выражение yield. Для запуска генератора нужно вызвать send(None)

Описание результата выполнения:

  • ①Создайте объект-генератор: функция, содержащая выражение yield, больше не будет функцией, а объект-генератор будет возвращен после вызова.

  • ②Запустите генератор: перед использованием генератора необходимо позвонить__next__илиsend(None), иначе будет сообщено об ошибке. После запуска генератора код будет выполняться до тех пор, покаyieldМесто, где он появляется, то есть исполнение доyield n, затем передать n вgenerator.__next__()Возвращаемое значение этой строки. (Обратите внимание, что генератор выполняется доyield nбудет останавливаться здесь до следующего запуска генератора)

  • ③ Отправить значение в выражение yield: вызовите метод отправки, чтобы отправить значение в выражение yield и возобновить выполнение генератора. Генератор продолжает работу с того места, где он остановился, и сталкивается со следующимyield, генератор снова приостанавливается и переключается на основную функцию для вывода send_result.

Ключом к пониманию этой демонстрации является то, что генератор запускает или возобновляет выполнение один раз, что будетyieldПриостановлено в. Шаг 2 выше выполняется толькоyield n, и оператор присваивания не выполняется. На шаге 3 генератор возобновляет выполнение, прежде чем датьyield_expression_valueНазначение.

Модель производителя и потребителя

В приведенном выше примере прерывание кода --> переключение выполнения отражает некоторые характеристики сопрограмм.

Возьмем другой пример производителя и потребителя. Этот пример взят изУчебное пособие по Python от Ляо Сюэфэна:

Традиционная модель производитель-потребитель заключается в том, что поток пишет сообщения, поток принимает сообщения и управляет очередями и ожиданием с помощью механизма блокировки, но если вы не будете осторожны, могут возникнуть взаимоблокировки.

Вместо этого используйте сопрограмму.После того, как производитель создает сообщение, он напрямую передаетyieldПерейти к потребителю, чтобы начать выполнение, и после того, как потребитель завершит выполнение, переключиться обратно на производителя, чтобы продолжить производство, что чрезвычайно эффективно.

def consumer():
    print("[CONSUMER] start")
    r = 'start'
    while True:
        n = yield r
        if not n:
            print("n is empty")
            continue
        print("[CONSUMER] Consumer is consuming %s" % n)
        r = "200 ok"


def producer(c):
    # 启动generator
    start_value = c.send(None)
    print(start_value)
    n = 0
    while n < 3:
        n += 1
        print("[PRODUCER] Producer is producing %d" % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    # 关闭generator
    c.close()


# 创建生成器
c = consumer()
# 传入generator
producer(c)

Результаты:

[CONSUMER] start
start
[PRODUCER] producer is producing 1
[CONSUMER] consumer is consuming 1
[PRODUCER] Consumer return: 200 ok
[PRODUCER] producer is producing 2
[CONSUMER] consumer is consuming 2
[PRODUCER] Consumer return: 200 ok
[PRODUCER] producer is producing 3
[CONSUMER] consumer is consuming 3
[PRODUCER] Consumer return: 200 ok

уведомлениеconsumerфункция представляет собойgenerator, положитьconsumerвходящийproduceЗадний:

  1. первый звонокc.send(None)запустить генератор;
  1. Затем, как только что-то будет произведено, передайтеc.send(n)Переключиться на потребительское исполнение;
  1. consumerпройти черезyieldПолучите сообщение, обработайте его и передайте сноваyieldвернуть результат;
  1. produceполучитьconsumerРезультат обработки, далее выдать следующее сообщение;
  1. produceРешил не производить, черезc.close()закрытиеconsumer, весь процесс заканчивается.

Весь процесс не блокируется и выполняется одним потоком,produceиconsumerЗадача выполняется совместно, поэтому она называется «сопрограммой», а не упреждающей многозадачностью потоков.

yield fromвыражение

Новое в Python 3.3yield fromграмматика, новая грамматика используется для делегирования операций одной части генератора другому генератору. Кроме того, подгенераторам (то есть «параметрам» после yield from) разрешено возвращать значение, которое может использоваться путем делегирования генераторов (то есть генераторов, которые включают yield from). А при делегировании генераторов можно оптимизировать подгенераторы.

Сначала рассмотрим самые простые приложения, такие как:

# 子生成器
def test(n):
    i = 0
    while i < n:
        yield i
        i += 1

# 委派生成器
def test_yield_from(n):
    print("test_yield_from start")
    yield from test(n)
    print("test_yield_from end")


for i in test_yield_from(3):
    print(i)

вывод:

test_yield_from start
0
1
2
test_yield_from end

Здесь мы просто добавляем в генератор немного печати, если это формальный код, то можно добавить нормальную логику выполнения.

если вышеtest_yield_fromВ функции дваyield fromоператор, будет выполняться последовательно. Например, приведенный вышеtest_yield_fromФункция переписывается следующим образом:

def test_yield_from(n):
    print("test_yield_from start")
    yield from test(n)
    print("test_yield_from doing")
    yield from test(n)
    print("test_yield_from end")

выведет:

test_yield_from start
0
1
2
test_yield_from doing
0
1
2
test_yield_from end

это здесь,yield fromЭффект эквивалентен сокращенной форме следующего написания

for item in test(n):
    yield item

выглядит такyield fromЭто не имело большого значения, на самом деле это помогло нам с исключениями и тому подобными вещами. Подробнее см. этот вопрос в stackoverflow:Каковы основные области применения нового синтаксиса «выход из» в Python 3.3 на практике?

Корутина

  • Начиная с Python 3.4, были добавлены API, связанные с асинхронностью, и используется синтаксис@asyncio.coroutineиyield fromРеализовать сопрограммы
  • Представлено в Python 3.5async/awaitсинтаксис см.PEP492

Давайте сначала посмотрим на реализацию Python 3.4.

@asyncio.coroutine

В Python 3.4 используйте@asyncio.coroutineДекорированные функции называются сопрограммами. Однако на грамматическом уровне это строго не ограничено.

Для тех, кто не знает о декораторах, вы можете прочитать мой последний блог--«Понимание декораторов Python»

Для сопрограмм, изначально поддерживаемых Python, Python делает некоторые различия между сопрограммами и генераторами, чтобы облегчить устранение неоднозначности этих двух разных, но связанных концепций:

  • отмечен@asyncio.coroutineФункция декоратора называется функцией сопрограммы,iscoroutinefunction()метод возвращает Истина
  • Объект, возвращаемый вызовом функции сопрограммы, называется объектом сопрограммы.iscoroutine()функция возвращает Истина

Возьмите каштан, мы даем вышеyield fromдобавить в демо@asyncio.coroutine:

import asyncio

...

@asyncio.coroutine
def test_yield_from(n):
    ...

# 是否是协程函数
print(asyncio.iscoroutinefunction(test_yield_from))
# 是否是协程对象
print(asyncio.iscoroutine(test_yield_from(3)))

Нет никаких сомнений в том, что вывод True.

можно посмотреть@asyncio.coroutineЧтобы увидеть, что он сделал, я упрощу его исходный код, который примерно выглядит следующим образом:

import functools
import types
import inspect

def coroutine(func):
    # 判断是否是生成器
    if inspect.isgeneratorfunction(func):
        coro = func
    else:
        # 将普通函数变成generator
        @functools.wraps(func)
        def coro(*args, **kw):
            res = func(*args, **kw)
            res = yield from res
            return res
    # 将generator转换成coroutine
    wrapper = types.coroutine(coro)
    # For iscoroutinefunction().
    wrapper._is_coroutine = True
    return wrapper

Пометка этого декоратора на генераторе превращает его в сопрограмму.

Тогда давайте на самом деле использовать@asyncio.coroutineиyield from:

import asyncio

@asyncio.coroutine
def compute(x, y):
    print("Compute %s + %s ..." % (x, y))
    yield from asyncio.sleep(1.0)
    return x + y

@asyncio.coroutine
def print_sum(x, y):
    result = yield from compute(x, y)
    print("%s + %s = %s" % (x, y, result))

loop = asyncio.get_event_loop()
print("start")
# 中断调用,直到协程执行结束
loop.run_until_complete(print_sum(1, 2))
print("end")
loop.close()

Результаты:

start
Compute 1 + 2 ...
1 + 2 = 3
end

print_sumПодпрограмма вызывается в этой сопрограммеcompute, он будет ждатьcomputeРезультат возвращается после завершения выполнения.

Процесс вызова этой демо-точки выглядит следующим образом:

tulip_coro.png

EventLoop поместитprint_sumИнкапсулирован как объект Task

Блок-схема показывает поток управления этой демонстрации, но не показывает все его детали. Например, единица «паузы» фактически создает будущий объект, а затем передаетBaseEventLoop.call_later()Разбудить эту задачу через 1 с.

В частности,@asyncio.coroutineБудет удалено в Python 3.10.

async/await

Python3.5 начал внедрятьasync/awaitграмматика(PEP 492), чтобы упростить использование сопрограмм и сделать их более понятными.

async/awaitна самом деле просто@asyncio.coroutineиyield fromсинтаксический сахар для:

  • Пучок@asyncio.coroutineзаменитьasync
  • Пучокyield fromзаменитьawait

Вот и все.

Например, приведенный выше пример:

import asyncio


async def compute(x, y):
    print("Compute %s + %s ..." % (x, y))
    await asyncio.sleep(1.0)
    return x + y


async def print_sum(x, y):
    result = await compute(x, y)
    print("%s + %s = %s" % (x, y, result))


loop = asyncio.get_event_loop()
print("start")
loop.run_until_complete(print_sum(1, 2))
print("end")
loop.close()

Давайте посмотрим на другой пример Future в asyncio:

import asyncio

future = asyncio.Future()


async def coro1():
    print("wait 1 second")
    await asyncio.sleep(1)
    print("set_result")
    future.set_result('data')


async def coro2():
    result = await future
    print(result)


loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait([
    coro1()
    coro2()
]))
loop.close()

Выходной результат:

wait 1 second
(大约等待1秒)
set_result
data

Здесь объект будущего, за которым следует ожидание, объект будущего может быть вызван после yield from или await в сопрограмме, Его функция состоит в том, чтобы приостановить сопрограмму до тех пор, пока выполнение будущего не завершится, не вернет результат или не вызовет исключение.

И в нашем случае,await futureдолжен подождатьfuture.set_result('data')может закончиться только после. будетcoro2()В качестве второй сопрограммы это может быть недостаточно очевидно, и вызов сопрограммы можно изменить на это:

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait([
    # coro1(),
    coro2(),
    coro1()
]))
loop.close()

Выход остается таким же, как указано выше.

фактически,asyncИспользование этого ключевого слова может использоваться не только в функциях, но и вasync withасинхронный менеджер контекста,async forАсинхронные итераторы. Если вы заинтересованы в них и считаете их полезными, вы можете найти информацию в Интернете. Я не буду здесь слишком много распространяться из-за нехватки места.

Суммировать

В этой статье было сделано некоторое изучение, исследование и краткое изложение генераторов и сопрограмм, но не было проведено много глубоких исследований. Правильно, и в качестве введения к заметке я попытаюсь реализовать асинхронный API самостоятельно, надеясь помочь в понимании и обучении.

Ссылка на ссылку

Сопрограммы Python https://thief.one/2017/02/20/Python%E5%8D%8F%E7%A8%8B/

woohoo.dab EA на .com/co rules/…

Coroutines

How the heck does async/await work in Python 3.5

Документация по сопрограмме Python3.4

Документация по сопрограмме Python3.5

Учебное пособие по Python от Ляо Сюэфэна — сопрограммы