1はじめに
ブログ投稿<Pythonの基本的な知識の組み合わせ-Pythonでのマルチプロセスとマルチスレッド>では、ジェネレーターを介してPythonコルーチンを実装する方法については説明していません。
コルーチンは並行プログラミングを実現する方法です。もちろん、マルチプロセス/マルチスレッドも並行性を解決する方法ですが、サーバーに同時に接続されているクライアントが特定のレベルに達すると、プロセスのコンテキスト切り替えが行われます。大量のリソースを消費し、スレッドもそのような大きなプレッシャーに耐えることができません。現時点では、タスクをスケジュールし、スレッドの開始、スレッドの管理、マルチスレッドでの同期ロックなどのさまざまなオーバーヘッドを節約するスケジューラが必要です。Nginxは、高い同時実行性の下で低リソース、低消費、および高パフォーマンスを維持できるかどうかは、スケジューラーに依存します(例:ポーリングアルゴリズム)。
Pythonでは、コルーチンを実装するためのジェネレーターの使用はPython2で一般的です。Python3.7以降では、asyncioとasync / awaitに基づく新しいメソッドが提供されています。今では2020[::-1]
数年になります(笑)。新しい機能、新しいコルーチン。
2.コルーチンの実装
2.1例1:クローラー
%time
これはインタプリタのシンタックスシュガーjupyter notebook
でipython
あり、ステートメントの実行時間をテストするために使用されます。
4つのタスクは合計で10秒かかります。次に、コルーチンを使用して同時実行を実現し、効率を最適化および改善します。
import asyncio
async def get_page(url):
print('acquire page {}'.format(url))
sleep_time = int(url.split('_')[-1])
await asyncio.sleep(sleep_time)
print('ok {}'.format(url))
async def main(urls):
for url in urls:
await get_page(url)
asyncio.run(main(['url_1','url_2','url_3','url_4']))
# 输出
acquire page url_1
ok url_1
acquire page url_2
ok url_2
acquire page url_3
ok url_3
acquire page url_4
ok url_4
Wall time: 10 s
Python 3.7以降、コルーチンが非同期プログラムを作成するのは非常に簡単です。コルーチンで使用されるほとんどのマジックメソッド
asyncio
はライブラリに含まれています。async
関数内に修飾子を使用して非同期関数を宣言してからawait
呼び出すだけです。
2.2アイデアを整理しましょう:
最初に、例ではimport asyncio
、パッケージのインポートを使用し、次にasync
宣言さget_page()
れたmain()
非同期関数を使用します。非同期関数を呼び出すと、コルーチンオブジェクトを取得します。
非同期関数を宣言した後、非同期関数を呼び出す必要があります。一般的に使用されるコルーチン実行メソッドは3つあります。
-
これを使用し
await
て呼び出すことができますawait
。実行の効果はPythonの通常の実行の効果と同じです。プログラムの実行後、ここでブロックされ、呼び出されたコルーチン関数に入り、実行が完了すると戻ります。これはまたawait
、意味。await asyncio.sleep(sleep_time)
ここで数秒間休むawait get_page(url)
ことを意味します。これは、get_page()関数を実行することを意味します。 -
asyncio.create_task()
タスクの作成にも使用できます。フォローアップでは、きちんとした並行プログラミングの詳細をブログに投稿する場合がありますが、ここではスキップします。 -
最後に、
asyncio.run
操作をトリガーするasyncio.run
ことにより、この関数はコルーチンのイベントループに注意を払うことなく、コルーチンを呼び出すのが非常に簡単になります。使用方法は、ソースコードの例を参照しています。Example: async def main(): await asyncio.sleep(1) print('hello') asyncio.run(main())
実行時間はまだ10秒であることがわかりましたか?ここで何が起こっているのですか?await
これは同期呼び出しであるため、get_page(url)
現在の呼び出しが終了した後、次の呼び出しはトリガーされません。これは、非同期インターフェイスを使用して同期コードを記述することと同じです。
ここではasyncio.create_task()
、非同期でタスクを作成するために使用します。
import asyncio
async def get_page(url):
print('acquire page {}'.format(url))
sleep_time = int(url.split('_')[-1])
await asyncio.sleep(sleep_time)
print('ok {}'.format(url))
async def main(urls):
tasks = [asyncio.create_task(get_page(url)) for url in urls]
for task in tasks:
await task
asyncio.run(main(['url_1','url_2','url_3','url_4']))
# 输出
acquire page url_1
acquire page url_2
acquire page url_3
acquire page url_4
ok url_1
ok url_2
ok url_3
ok url_4
Wall time: 3.66 s
明らかに、出力結果を比較すると、4つのタスクはほぼ同時に作成されます。タスクは作成後すぐに実行されるようにスケジュールされており、タスクコードはここでブロックされないため、すべてのタスクが完了するまで待つ必要があります。実行する前にfor task in tasks:await task
。
明らかに、マルチスレッドと比較して、コルーチンの記述方法は一目で明確になります。task
タスクの場合、実際には別の記述方法があります。見てみましょう。
import asyncio
async def get_page(url):
print('acquire page {}'.format(url))
sleep_time = int(url.split('_')[-1])
await asyncio.sleep(sleep_time)
print('ok {}'.format(url))
async def main(urls):
tasks = [asyncio.create_task(get_page(url)) for url in urls]
await asyncio.gather(*tasks)# 一个解包操作
asyncio.run(main(['url_1','url_2','url_3','url_4']))
# 输出
acquire page url_1
acquire page url_2
acquire page url_3
acquire page url_4
ok url_1
ok url_2
ok url_3
ok url_4
Wall time: 3.66 s
2.3まとめ
前のコードと比較して*tasks
、リストを関数パラメーターに変換するもう1つの解凍操作があります。これ**tasks
は、辞書を関数パラメーターに変換します。
比較するとコルーチンの作成、提供した後、、読んで簡単に理解するために、内部実装に焦点を当てる必要はありませんが、より多くのコード自体(読み取り、読み取り、ような感触を心配する古いインターフェースに比べてと感じ、ハッハッハハッハッハ)python2
yield
python3.7
asyncio.create_task()
asyncio.run()
await
numpy
pytorch
3.コルーチンの基本的な実装
3.1例2
import asyncio
async def work_1():
print('work 1 start ')
await asyncio.sleep(1)
print('work 1 is done!')
async def work_2():
print('work 2 start ')
await asyncio.sleep(2)
print('work 2 is done')
async def main():
print('before await ')
await work_1()
print('awaited work_1')
await work_2()
print('awaited work_2')
asyncio.run(main())
# 输出
before await
work 1 start
work 1 is done!
awaited work_1
work 2 start
work 2 is done
awaited work_2
3.2例3
import asyncio
async def work_1():
print('work 1 start ')
await asyncio.sleep(1)
print('work 1 is done!')
async def work_2():
print('work 2 start ')
await asyncio.sleep(2)
print('work 2 is done')
async def main():
task1 = asyncio.create_task(work_1())
task2 = asyncio.create_task(work_2())
print('before await ')
await task1
print('awaited work 1')
await task2
print('awaited work 2')
asyncio.run(main())
# 输出
before await
work 1 start
work 2 start
work 1 is done!
awaited work 1
work 2 is done
awaited work 2
例2と例3の実行順序は異なりますか?
asyncio.run(main())
プログラムがmain()関数に入り、イベントループが開始することを示します。- Task1およびtask2タスクが作成され、イベントループに入って待機してから、
print('before await ')
; await task1
実行すると、ユーザーは現在のメインタスクから切り離すことを選択し、イベントスケジューラはwork_1のスケジュールを開始します。- work_1は、実行を開始し、実行
print('work 1 start ')
して、実行await asyncio.sleep(1)
スケジュールwork_2に、現在のタスクから切り出し、およびイベントスケジューラを開始します。 - work_2は、実行を開始実行
print('work 2 start ')
、その後、実行しawait asyncio.sleep(2)
、現在のタスクから切り出します。 - 上記のすべてのイベントの実行時間は1ms〜10ms、またはそれより短くする必要があり、イベントスケジューラはこの時間からスケジューリングを一時停止します。
- 1秒後、work_1のスリープが終了し、イベントスケジューラが制御をtask_1に再転送し、出力し
work 1 is done!
、task_1がタスクを完了し、イベントループを終了します。 await task1
終了すると、イベントスケジューラはコントローラをメインタスクに渡し、それを出力してからawaited work 1
、waittask2で待機します。- 2秒後、work_2のスリープが終了し、イベントスケジューラが制御をtask_2に再転送し、出力し
work 2 is done!
、task_2が完了し、イベントループを終了します。 - メインタスクが出力され
awaited work 2
、コルーチンタスクが完了し、イベントループが終了します。
3.3タイムアウトタスク
Pythonでクローラーを構成する場合、タスクのクロール中に問題が発生した場合はどうすればよいですか?最も簡単な方法は、時間をかけてキャンセルすることです。どうすればよいですか?
import asyncio
async def work_1():
await asyncio.sleep(1)
return 1
async def work_2():
await asyncio.sleep(2)
return 2/0
async def work_3():
await asyncio.sleep(3)
return 3
async def main():
task_1 = asyncio.create_task(work_1())
task_2 = asyncio.create_task(work_2())
task_3 = asyncio.create_task(work_3())
await asyncio.sleep(2)
task_3.cancel()
res = await asyncio.gather(task_1,task_2,task_3,return_exceptions=True)
print(res)
asyncio.run(main())
# 输出
[1, ZeroDivisionError('division by zero'), CancelledError()]
上記の例では、work_1は正常に動作しており、work_2中にエラーが発生し、work_3の実行時間が長すぎてキャンセルしました。情報はresに返され、出力されて設定されましたreturn_exceptions=True
。設定されていない場合True
つまり、例外をキャッチする必要があり、実行を続行することはできません。
3.4生産者/消費者モデル
import asyncio
import random
async def consumer(queque,id):
while True:
val = await queque.get()
print('{} get a val: {} '.format(id,val))
await asyncio.sleep(1)
async def producer(queue,id):
for i in range(5):
val = random.randint(1,10)
await queue.put(val)
print('{} put a val : {}'.format(id,val))
await asyncio.sleep(1)
async def main():
queue = asyncio.Queue()
consumer_1 = asyncio.create_task(consumer(queue,'consumer_1'))
consumer_2 = asyncio.create_task(consumer(queue,'consumer_2'))
producer_1 = asyncio.create_task(producer(queue,'producer_1'))
producer_2 = asyncio.create_task(producer(queue,'producer_2'))
await asyncio.sleep(10)
consumer_1.cancel()
consumer_2.cancel()
await asyncio.gather(consumer_1,consumer_2,producer_1,producer_2,return_exceptions=True)
asyncio.run(main())
# 输出
producer_1 put a val : 1
producer_2 put a val : 1
consumer_1 get a val: 1
consumer_2 get a val: 1
producer_1 put a val : 2
producer_2 put a val : 2
consumer_1 get a val: 2
consumer_2 get a val: 2
producer_1 put a val : 6
producer_2 put a val : 10
consumer_1 get a val: 6
consumer_2 get a val: 10
producer_1 put a val : 8
producer_2 put a val : 2
consumer_1 get a val: 8
consumer_2 get a val: 2
producer_1 put a val : 9
producer_2 put a val : 1
consumer_1 get a val: 9
consumer_2 get a val: 1
4.まとめ
- コルーチンとマルチスレッドの違い:①コルーチンはシングルスレッドです。②コルーチンは、いつ制御を渡して次のタスクに切り替えるかをユーザーが決定します。
- Python 3.7バージョン以降、コルーチンの記述方法はよりシンプルになりました。ライブラリ
asyncio
内のasync/await
合計と組み合わせると、create_task
中小規模の並行プログラミングにプレッシャーはかかりません。 - コルーチンの使用、I / Oの待機を一時停止するタイミング、および最後まで実行するタイミングには、イベントループの概念が必要です。
ブログ投稿のフォローアップ更新については、私の個人ブログをフォローしてください:Stardust Blog