Введение
Обычно мы думаем о потоках как о легковесных процессах, поэтому мы также понимаем сопрограммы как легковесные потоки или микропотоки.
Обычно в 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 завершено.В примерах программ сон используется для представления асинхронного ввода-вывода.В реальных проектах сопрограммы могут использоваться для асинхронного чтения и записи сетей, чтения и записи файлов и рендеринга интерфейсов. в ожидании сопрограмм, в то же время процессор может выполнять и другие вычисления, и здесь роль сопрограммы. Так в чем же разница между сопрограммами и многопоточностью? Переключение многопоточности должно выполняться операционной системой, когда потоков становится все больше и больше, стоимость переключения будет очень высокой, а сопрограмма переключается внутри потока, процесс переключения контролируется нами, поэтому накладные расходы намного меньше.Это сопрограмма.Фундаментальное различие между обработкой и многопоточностью.