コルーチンは、並行プログラミングを実装する方法です。Python 3.7以降では、コルーチンを使用して非同期プログラムを作成するのは非常に簡単です。
26.1同期および非同期
まず、同期(同期)と非同期(非同期)の概念を区別しましょう。
いわゆる同期とは、操作が次々に実行され、次の操作は前の操作が完了するのを待ってから実行できることを意味します。
非同期とは、異なる操作を交互に実行できることを意味します。操作の1つがブロックされた場合、プログラムは待機しませんが、実行可能な操作を見つけて実行を継続します。これにより、効率を向上させることができる。
26.2非同期の原則
Pythonは、非同期プログラミングにAsyncioライブラリを使用します。Asyncioは、メインスレッドで複数の異なるタスクを交互に実行できます。ここでのタスクは、特別なfutureオブジェクトです。これらのさまざまなタスクは、イベントループと呼ばれるオブジェクトによって制御されます。
- 異なるタスクを交互に実行できます。タスクの1つがブロックされている場合、プログラムは待機せず、event_loopは実行可能タスクを見つけて実行を継続します。
- タスクには準備完了状態と待機状態があります。タスクが待機状態になると、event_loopは実行可能な他のタスクに切り替わります。
Asyncio切り替えタスクの詳細を見てみましょう。
タスクには2つの主要な状態があります。1つは準備完了状態で、もう1つは待機状態です。準備完了状態とは、タスクをいつでも実行できる状態であることを意味します。待機状態とは、タスクが実行されているが、I/O操作が完了するのを待機している状態を指します。
イベントループは2つのタスクリストを維持し、タスクを準備完了状態で保存し、タスクを待機状態で保存します。イベントループは、準備完了状態のタスクリストからタスクを選択し、タスクが制御をイベントループに戻すまでタスクを実行します。
タスクが制御をイベントループに戻すと、イベントループは、タスクが完了したかどうかに応じて、タスクを準備完了状態リストまたは待機状態リストに入れ、待機状態リスト内のタスクをトラバースして、タスクが完了したかどうかを確認します。 。
完了した場合は、準備完了状態リストに入れます。完了していない場合は、待機状態リストに進みます。
したがって、すべてのタスクが適切なリストに再配置されると、新しいサイクルが始まります。イベントループは、すべてのタスクが完了するまで、準備完了状態リストからタスクを選択して実行し続けます。
Asyncioの場合、実行時にいくつかの外部要因によってタスクが中断されないため、Asyncioでの操作には競合状態がなく、スレッドの安全性について心配する必要はありません。
26.3コルーチン実行プロセス(ジャンプ)
import asyncio
import time
async def worker_1(): # def前面加一个async表示是个异步函数
print('worker_1 start')
await asyncio.sleep(1) # ⑤ 遇到 await,事件调度器切断当前任务执行
print('worker_1 done') # ⑧ worker_1彻底执行完毕,调度器去调度worker_2
async def worker_2():
print('worker_2 start')
await asyncio.sleep(2) # ⑦ 遇到 await,事件调度器切断当前任务执行,去调度worker_1
print('worker_2 done') # ⑨ 【1】秒钟后,worker_1彻底执行完毕,等切回到worker_2时在等1秒就好了
async def main():
task1 = asyncio.create_task(worker_1()) # ② 创建两个task,立即进入事件循环等待被调度
task2 = asyncio.create_task(worker_2())
print('before await') # ③ 执行到此
await task1 # ④ 从当前的主任务中切出,事件调度器开始调度worker_1
print('awaited worker_1')
await task2 # ⑥ 事件调度器开始调度worker_2
print('awaited worker_2') # ⑩ 主任务输出 'awaited worker_2',协程全任务结束,事件循环结束
start = time.time()
asyncio.run(main()) # ①程序进入 main() 函数,事件循环开启;
print("Escape {}".format(time.time() - start)) # 一共花2秒
26.4コーディングルーチン
import time
import asyncio # ① 首先 import asyncio
async def crawl_page(url): # ② async 声明异步函数
print('crawling {}'.format(url))
sleep_time = int(url.split('_')[-1])
await asyncio.sleep(sleep_time) # ③ await进入被调用的协程函数,执行完毕返回后再继续,会阻塞
print('OK {}'.format(url))
return sleep_time
async def main(urls):
tasks = []
for url in urls:
coroutine_object = crawl_page(url) # ④调用异步函数crawl_page并不会执行,而是会得到一个协程对象coroutine_object
tasks.append(asyncio.create_task(coroutine_object)) # ⑤create_task将协程对象制作成任务,并立即执行
res = await asyncio.gather(*tasks,
return_exceptions=True) # ⑤阻塞在这,等所有任务都结束,return_exceptions=True可以避免个别协程对象执行时出错影响大局
return res # ⑥ 返回每一个协程对象的执行结果
if __name__ == '__main__':
start = time.time()
result = asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4'])) # asyncio.run执行协程
print("Escape {}".format(time.time() - start))
print(result)
26.5サンプルアプリケーション
asyncio.queueモジュールは、複数の本番コルーチンと複数の消費コルーチンのキューを実装します。
26.5.1生産者/消費者モデル
import asyncio
import random
async def consumer(queue, ids):
while True:
value = await queue.get()
print('{} get a val: {}'.format(ids, value))
await asyncio.sleep(1)
async def producer(queue, ids):
for i in range(5):
val = random.randint(1, 10)
await queue.put(val)
print('{} put a val: {}'.format(ids, val))
await asyncio.sleep(1)
async def main():
queue = asyncio.Queue()
consumer1 = asyncio.create_task(consumer(queue, "consumer1"))
consumer2 = asyncio.create_task(consumer(queue, "consumer2"))
producer1 = asyncio.create_task(producer(queue, "producer1"))
producer2 = asyncio.create_task(producer(queue, "producer2"))
await asyncio.sleep(10)
consumer2.cancel()
consumer1.cancel()
await asyncio.gather(consumer1, consumer2, producer1, producer2, return_exceptions=True)
asyncio.run(main())
26.5.2同時クローラー
import asyncio
import aiohttp
from bs4 import BeautifulSoup
async def fetch_content(url):
async with aiohttp.ClientSession(
headers=header, connector=aiohttp.TCPConnector(ssl=False)
) as session:
async with session.get(url) as response:
return await response.text()
async def main():
url = "https://movie.douban.com/cinema/later/beijing/"
init_page = await fetch_content(url)
init_soup = BeautifulSoup(init_page, 'lxml')
movie_names, urls_to_fetch, movie_dates = [], [], []
all_movies = init_soup.find('div', id="showing-soon")
for each_movie in all_movies.find_all('div', class_="item"):
all_a_tag = each_movie.find_all('a')
all_li_tag = each_movie.find_all('li')
movie_names.append(all_a_tag[1].text)
urls_to_fetch.append(all_a_tag[1]['href'])
movie_dates.append(all_li_tag[0].text)
tasks = [fetch_content(url) for url in urls_to_fetch]
pages = await asyncio.gather(*tasks)
for movie_name, movie_date, page in zip(movie_names, movie_dates, pages):
soup_item = BeautifulSoup(page, 'lxml')
img_tag = soup_item.find('img')
print('{} {} {}'.format(movie_name, movie_date, img_tag['src']))
asyncio.run(main())
26.6互換性の問題
- asynicio互換ライブラリを使用するには
- httpのcleintはaiohttpライブラリを使用します。その理由は、requestsライブラリはasyncioと互換性がありませんが、aiohttpライブラリは互換性があるためです。
26.7マルチプロセス、マルチスレッド、またはコルーチンを選択するにはどうすればよいですか?
- I / Oバウンドであり、I / O操作が遅く、多くのタスク/スレッドを協調して実装する必要がある場合は、asyncioの方が適しています。
- I / Oバウンドであるが、I / O操作が高速で、必要なタスク/スレッドの数が限られている場合は、マルチスレッドで問題ありません。
- CPUにバインドされている場合は、プログラムの効率を向上させるために複数のプロセスを使用する必要があります。