Подробное объяснение сопрограмм Python

Python

Введение

Обычно мы думаем о потоках как о легковесных процессах, поэтому мы также понимаем сопрограммы как легковесные потоки или микропотоки.

Обычно в Python мы обычно используем многопоточность или многопроцессность для реализации параллельного программирования.Для вычислительных задач мы обычно используем многопроцессорность для реализации из-за существования GIL, а для задач типа ввода-вывода мы можем использовать планирование потоков to let threads Выдает GIL при выполнении задач ввода-вывода, тем самым достигая кажущегося параллелизма. На самом деле, для задач типа ввода-вывода у нас также есть выбор сопрограмм.Сопрограммы — это «параллелизм», работающий в одном потоке.Одно из преимуществ сопрограмм по сравнению с многопоточностью заключается в том, что они экономят накладные расходы на переключение между многопоточностью и получают большую операционную эффективность. .

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

Каковы преимущества сопрограмм?

  • Эффективность выполнения чрезвычайно высока, так как переключение подпрограмм (функция) не является переключением потоков, оно управляется самой программой, и нет накладных расходов на переключение потоков. Следовательно, по сравнению с многопоточностью, чем больше потоков, тем очевиднее преимущество производительности сопрограмм.
  • Нет необходимости в многопоточном механизме блокировки, потому что поток всего один, и нет конфликта записи переменных одновременно, и не требуется блокировки при управлении общими ресурсами, поэтому эффективность выполнения гораздо выше.

Сопрограммы могут работать с эффективностью программ с интенсивным вводом-выводом, но работа с программами с интенсивным использованием ЦП не является их сильной стороной.Чтобы в полной мере использовать загрузку ЦП, вы можете комбинировать несколько процессов + сопрограммы.

Корутины в Python прошли долгий путь. Он прошел примерно следующие три стадии:

  • Исходная деформация генератора выход/посылка
  • Ввести @asyncio.coroutine и выйти из
  • Введите ключевое слово async/await

Выше приведены некоторые сведения о концепции и преимуществах сопрограмм. Это кажется более абстрактным. Python2.x имеет ограниченную поддержку сопрограмм. Генератор yield реализован частично, но не полностью. Модуль gevent имеет лучшую реализацию; Python3.4 добавлен asyncio введен модуль, а в Python 3.5 реализована поддержка уровня синтаксиса async/await, модуль asyncio в Python 3.6 более полный и стабильный. Далее мы подробно остановимся на этом содержании.

Сопрограммы Python2.x

Python2.x реализует сопрограммы следующими способами:

  • yield + send
  • gevent (см. последующие главы)

yield + send (с использованием генераторов для реализации сопрограмм)

Давайте посмотрим на применение сопрограмм через модель "производитель-потребитель". После того, как производитель создает сообщение, оно напрямую переходит к потребителю через yield для начала выполнения. После того, как потребитель заканчивает выполнение, он переключается обратно на производителя для продолжить производство.

#-*- coding:utf8 -*-
def consumer():
    r = ''
    while True:
    	n = yield r
    	if not n:
    	    return
    	print('[CONSUMER]Consuming %s...' % n)
    	r = '200 OK'

def producer(c):
    # 启动生成器
    c.send(None)
    n = 0
    while n < 5:
    	n = n + 1
    	print('[PRODUCER]Producing %s...' % n)
    	r = c.send(n)
    	print('[PRODUCER]Consumer return: %s' % r)
    c.close()

if __name__ == '__main__':
    c = consumer()
    producer(c)

send(msg)иnext()Разница в том, чтоsendпараметры могут передаватьсяyieldвыражение, переданный параметр будет использоваться какyieldзначение выражения иyieldПараметр — это значение, возвращаемое вызывающей стороне. Другими словами, этоsendМожно принудительно изменить предыдущийyieldзначение выражения. Например, есть функцияyieldназначатьa = yield 5, первая итерация здесь вернет 5, a не было присвоено значение. На второй итерации используйтеsend(10), то он вынужден изменитьyield 5Значение выражения равно 10, которое изначально было равно 5, а результатa = 10.send(msg)иnext()У всех есть возвращаемые значения, и их возвращаемое значение — это текущая встречающаяся итерация.yieldчас,yieldЗначение следующего выражения фактически является значением в текущей итерации.yieldследующие параметры. первый звонокsendдолжно бытьsend(None), иначе будет сообщено об ошибке, причинаNoneпотому что еще нет ни одногоyieldВыражения могут использоваться для присвоения значений. Вывод приведенного выше примера выглядит следующим образом:

[PRODUCER]Producing 1...
[CONSUMER]Consuming 1...
[PRODUCER]Consumer return: 200 OK
[PRODUCER]Producing 2...
[CONSUMER]Consuming 2...
[PRODUCER]Consumer return: 200 OK
[PRODUCER]Producing 3...
[CONSUMER]Consuming 3...
[PRODUCER]Consumer return: 200 OK
[PRODUCER]Producing 4...
[CONSUMER]Consuming 4...
[PRODUCER]Consumer return: 200 OK
[PRODUCER]Producing 5...
[CONSUMER]Consuming 5...
[PRODUCER]Consumer return: 200 OK

Сопрограммы Python3.x

Помимо реализации сопрограмм в Python2.x, Python3.x также предоставляет следующие способы реализации сопрограмм:

  • asyncio + yield from (python3.4+)
  • asyncio + async/await (python3.5+)

После Python 3.4 был представлен модуль asyncio, который вполне может поддерживать сопрограммы.

asyncio + yield from

asyncio — это стандартная библиотека, представленная в Python 3.4, со встроенной поддержкой асинхронного ввода-вывода. Асинхронная операция asyncio должна быть передана в сопрограмму.yield fromЗаканчивать. См. следующий код (необходимо использовать в Python 3.4 и более поздних версиях):

#-*- coding:utf8 -*-
import asyncio

@asyncio.coroutine
def test(i):
    print('test_1', i)
    r = yield from asyncio.sleep(1)
    print('test_2', i)

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = [test(i) for i in range(3)]
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()

@asyncio.coroutineПометьте генератор как тип сопрограммы, а затем добавьте сопрограмму в EventLoop для выполнения. test() сначала распечатает test_1, затемyield fromСинтаксис позволяет нам легко вызвать другой генератор. так какasyncio.sleep()Также сопрограмма, поэтому поток не будет ждатьasyncio.sleep(), но напрямую прерывает и выполняет следующий цикл обработки сообщений. когдаasyncio.sleep()При возврате поток можетyield fromПолучите возвращаемое значение (здесь нет), а затем выполните следующую строку оператора. Пучокasyncio.sleep(1)Это считается операцией ввода-вывода, которая занимает секунду 1. В течение этого периода основной поток не ждет, а выполняет другие исполняемые сопрограммы в EventLoop, поэтому может быть достигнуто параллельное выполнение.

asyncio + async/await

Чтобы упростить и лучше идентифицировать асинхронный ввод-вывод, начиная с Python 3.5 были введены новые синтаксисы async и await, которые могут сделать код сопрограммы более кратким и читабельным. Обратите внимание, что async и await — это новые синтаксисы для сопрограмм, и использование нового синтаксиса требует только двух простых замен:

  • Замените @asyncio.coroutine на async
  • Замените yield from на await

См. следующий код (используется в Python 3.5 и выше):

#-*- coding:utf8 -*-
import asyncio

async def test(i):
    print('test_1', i)
    await asyncio.sleep(1)
    print('test_2', i)
    
if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = [test(i) for i in range(3)]
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()

Результат работы такой же, как и раньше. По сравнению с предыдущим разделом здесь просто замените yield from на await, @asyncio.coroutine на async, а остальное останется без изменений.

Gevent

Gevent — это сетевая библиотека, основанная на Greenlet, которая реализует сопрограммы через гринлет. Основная идея заключается в том, что гринлет считается сопрограммой.Когда гринлет сталкивается с операцией ввода-вывода, такой как доступ к сети, он автоматически переключается на другие гринлеты, ждет завершения операции ввода-вывода, а затем переключается обратно, чтобы продолжить выполнение в подходящее время. Так как операция ввода-вывода занимает очень много времени, программа часто находится в состоянии ожидания.Благодаря тому, что gevent автоматически переключает сопрограмму за нас, гарантируется, что всегда будут запущены гринлеты, а не ожидание операции ввода-вывода.

Как модуль расширения C, Greenlet инкапсулирует API цикла обработки событий libevent, позволяя разработчикам писать асинхронный код ввода-вывода синхронно, не меняя своих привычек программирования.

#-*- coding:utf8 -*-
import gevent

def test(n):
    for i in range(n):
        print(gevent.getcurrent(), i)

if __name__ == '__main__':
    g1 = gevent.spawn(test, 3)
    g2 = gevent.spawn(test, 3)
    g3 = gevent.spawn(test, 3)
    
    g1.join()
    g2.join()
    g3.join()

результат операции:

<Greenlet at 0x10a6eea60: test(3)> 0
<Greenlet at 0x10a6eea60: test(3)> 1
<Greenlet at 0x10a6eea60: test(3)> 2
<Greenlet at 0x10a6eed58: test(3)> 0
<Greenlet at 0x10a6eed58: test(3)> 1
<Greenlet at 0x10a6eed58: test(3)> 2
<Greenlet at 0x10a6eedf0: test(3)> 0
<Greenlet at 0x10a6eedf0: test(3)> 1
<Greenlet at 0x10a6eedf0: test(3)> 2

Видно, что 3 гринлета работают последовательно, а не попеременно. Чтобы гринлеты запускались попеременно, вы можете передатьgevent.sleep()Чтобы отказаться от контроля:

def test(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        gevent.sleep(1)

результат операции:

<Greenlet at 0x10382da60: test(3)> 0
<Greenlet at 0x10382dd58: test(3)> 0
<Greenlet at 0x10382ddf0: test(3)> 0
<Greenlet at 0x10382da60: test(3)> 1
<Greenlet at 0x10382dd58: test(3)> 1
<Greenlet at 0x10382ddf0: test(3)> 1
<Greenlet at 0x10382da60: test(3)> 2
<Greenlet at 0x10382dd58: test(3)> 2
<Greenlet at 0x10382ddf0: test(3)> 2

Конечно, в реальном коде мы бы не использовалиgevent.sleep()Чтобы переключить сопрограмму, gevent автоматически завершается при выполнении операции ввода-вывода, поэтому gevent необходимо изменить режим работы некоторых стандартных библиотек, поставляемых с Python, с блокирующего вызова на совместную операцию. Это делается с помощью патча обезьяны при запуске:

#-*- coding:utf8 -*-
from gevent import monkey; monkey.patch_all()
from urllib import request
import gevent

def test(url):
    print('Get: %s' % url)
    response = request.urlopen(url)
    content = response.read().decode('utf8')
    print('%d bytes received from %s.' % (len(content), url))
    
if __name__ == '__main__':
    gevent.joinall([
    	gevent.spawn(test, 'http://httpbin.org/ip'),
    	gevent.spawn(test, 'http://httpbin.org/uuid'),
    	gevent.spawn(test, 'http://httpbin.org/user-agent')
    ])

результат операции:

Get: http://httpbin.org/ip
Get: http://httpbin.org/uuid
Get: http://httpbin.org/user-agent
53 bytes received from http://httpbin.org/uuid.
40 bytes received from http://httpbin.org/user-agent.
31 bytes received from http://httpbin.org/ip.

Судя по результатам, одновременно выполняются 3 сетевые операции, а порядок окончания отличается, но поток всего один.

Суммировать

На данный момент введение сопрограмм в Python завершено.В примерах программ сон используется для представления асинхронного ввода-вывода.В реальных проектах сопрограммы могут использоваться для асинхронного чтения и записи сетей, чтения и записи файлов и рендеринга интерфейсов. в ожидании сопрограмм, в то же время процессор может выполнять и другие вычисления, и здесь роль сопрограммы. Так в чем же разница между сопрограммами и многопоточностью? Переключение многопоточности должно выполняться операционной системой, когда потоков становится все больше и больше, стоимость переключения будет очень высокой, а сопрограмма переключается внутри потока, процесс переключения контролируется нами, поэтому накладные расходы намного меньше.Это сопрограмма.Фундаментальное различие между обработкой и многопоточностью.