【Python】コルーチンを0から1まで学習する事例あり、コルーチンを徹底理解、リソース消費も増えず、効果はマルチスレッドに近い


序文

実際のプログラミングではマルチプロセスやマルチスレッドがよく使われますが、この記事はコルーチンを学習した経験と経験を記録し、それを実現するための記事を目指すことを目的としています。


コルーチンの利点については多くを語る必要はありませんが、コルーチンは I/O 集約型の武器であると言えます。実際、IO 集約型のタスクにはコルーチンという選択肢もあります。コルーチンは、マイクロスレッド、英語名Coroutineとも呼ばれ、シングルスレッドで動作する「同時実行」であり、マルチスレッドと比較して、マルチスレッド間の切り替えのオーバーヘッドを節約し、より高いパフォーマンスが得られることが大きな利点です。運用効率。Python の非同期 IO モジュール asyncio は、基本的なコルーチン モジュールです。
コルーチンの切り替えはスレッドの切り替えとは異なり、プログラム自体によって制御され、切り替えのオーバーヘッドはありません。コルーチンはすべて同じスレッドで実行されるため、マルチスレッドのロック機構を必要とせず、同時にデータにアクセスする問題がなく、実行効率はマルチスレッドよりもはるかに高くなります。

1.Pythonジェネレーター

1.1 Python ジェネレーターの概要

3 つの概念: 反復/反復子/反復可能オブジェクト
ジェネレーターとは何ですか?
ジェネレーターも反復可能オブジェクトです

[num for num in range(10)]
(num for num in range(10) )

上はリスト、下はジェネレーターです
ここに画像の説明を挿入

発電機の特徴

  1. 反復が完了すると、最後の反復を指し、再度反復されることはありません。リストとは異なります。
    ここに画像の説明を挿入
  2. 占有メモリ サイズが異なる
import sys
sys.getsizeof(10)
sys.getsizeof("Hello World")
sys.getsizeof(l)
sys.getsizeof(gen)
gen = (num for num in range(10000))
l = [num for num in range(10000)]

sys.getsizeof(l)
sys.getsizeof(gen)

ここに画像の説明を挿入
ジェネレーターが占有するメモリはほとんど変化しませんが、リストは要素が増えるにつれて増加します。

ジェネレータはリストと非常によく似ており、同じ反復方法を持ち、メモリ モデルによりメモリ スペースが節約され、ジェネレータは 1 回しか走査できません。遅延計算を実装でき、ジェネレーター コードがより簡潔になります。

1.2 キーワードの収量/収量

Php、js、Python C++ にはすべてこのキーワードがあり、これは比較的高度な文法現象です。
yield は関数内でのみ使用できます

def func():
	print("Hello World!");

type(func) 出力 <クラス '関数'>

def func():
	print("Hello World!");
	yield()

func () を呼び出すと、
ここに画像の説明を挿入
型 (func ()) に注意してジェネレーターが返されます。
ここに画像の説明を挿入
通常の関数呼び出しは None を返します。これは、関数が最初から何も返さないのは通常の関数の実行だからです。
yield は関数をジェネレーターに変えます。

def func():
	for i in range(10):
		print(i)


def func2():
	for i in range(10):
		yield i

ここに画像の説明を挿入

for i in func2():
	print(i)

0から9までの10個の数字も生成されます。

1.3 次へ/送信機能

マルチプロセスとマルチスレッドはオペレーティング システムの機能を反映し、コルーチンはプログラマのプロセス制御機能を反映します。次の例では、2 人のワーカー A と B が 2 つの作業タスクを交互にシミュレートし、マルチスレッドと同様の機能を単一のスレッドで実現しています。

import time

def task1():
    while True:
        yield "<甲>也累了,让<乙>工作一会儿"
        time.sleep(1)
        print("<甲>工作了一段时间.....")


def task2(t):
    next(t)
    while True:
        print("-----------------------------------")
        print("<乙>工作了一段时间.....")
        time.sleep(2)
        print("<乙>累了,让<甲>工作一会儿....")
        ret = t.send(None)
        print(ret)
    t.close()

if __name__ == '__main__':
    t = task1()
    task2(t)

出力:

<乙>工作了一段时间.....
<乙>累了,让<甲>工作一会儿....
<甲>工作了一段时间.....
<甲>也累了,让<乙>工作一会儿
-----------------------------------
<乙>工作了一段时间.....
<乙>累了,让<甲>工作一会儿....
<甲>工作了一段时间.....
<甲>也累了,让<乙>工作一会儿
-----------------------------------
<乙>工作了一段时间.....
<乙>累了,让<甲>工作一会儿....
<甲>工作了一段时间.....
<甲>也累了,让<乙>工作一会儿
-----------------------------------
<乙>工作了一段时间.....

質問 B のステートメントが最初に実行され、次に A が実行される理由を考えてください。
t = task1() はジェネレーターを返すだけです。task2(t)、next(t) の操作により、実行のために task1 に入り、yield に遭遇すると停止します。このときの next(t) の戻り値は次のとおりです: <A> も疲れているので、<B> を動作させます。しばらく待ってから
task2 の while True に入り、t.send(None) に遭遇するまで実行権が task1 に逆転され、次の yield 文が実行されます。
The send here can't pass none.
初期の頃、Python ではジェネレーターを作成するための yield キーワードが提供されていました。言い換えれば、yield を含む関数はジェネレーターです。
yield の文法規則は次のとおりです。yield のここで関数の実行を一時停止し、next() メソッドによって再度呼び出されるまで、yield の後に式の値を返します (デフォルトは None)。最後に一時停止された利回りコード。
各ジェネレーターは、send() メソッドを実装して、ジェネレーター内の yield ステートメントのデータを送信できます。現時点では、yield ステートメントは、yield xxxx の形式だけでなく、var = yield xxxx の代入の形式にすることもできます。これには 2 つの関数が同時にあり、1 つは関数を一時停止して返すことで、もう 1 つは外部の send() メソッドによって送信された値を受信し、関数を再アクティブ化し、この値を var 変数に割り当てることです。

def simple_coroutine():
    print('-> 启动协程')
    y = 10
    x = yield y
    print('-> 协程接收到了x的值:', x)

my_coro = simple_coroutine()
ret = next(my_coro)
print(ret)
my_coro.send(100)
-> 启动协程
10
-> 协程接收到了x的值: 1000
Traceback (most recent call last):
  File "c:\Users\jianming_ge\Desktop\碳汇算法第一版\carbon_code\单线程模拟多线程.py", line 54, in <module>
    my_coro.send(1000)
StopIteration

コルーチンは、次の 4 つの状態のいずれかになります。現在の状態は検査モジュールにインポートし、inspect.getgeneratorstate(…) メソッドを使用して確認できます。このメソッドは次の文字列のいずれかを返します。

「GEN_CREATED」は実行の開始を待機しています。

'GEN_RUNNING' コルーチンは実行中です。

「GEN_SUSPENDED」は、yield 式で一時停止します。

'GEN_CLOSED' 実行が終了しました。

send() メソッドのパラメータは一時停止された yield 式の値になるため、send() メソッドは、my_coro.send(10) など、コルーチンが一時停止されている場合にのみ呼び出すことができます。ただし、コルーチンがまだアクティブ化されていない場合 (ステータスが「GEN_CREATED」)、すぐに None 以外の値を送信すると、TypeError が発生します。したがって、最初に next(my_coro) を呼び出してコルーチンをアクティブにする必要があります (my_coro.send(None) を呼び出すこともできます)。このプロセスは事前アクティブ化と呼ばれます。

send() メソッドに加えて、実際には throw() メソッドと close() メソッドがあります。

generator.throw(exc_type[, exc_value[,traceback]])
ジェネレーターは、一時停止された yield 式で指定された例外をスローします。ジェネレーターがスローされた例外を処理する場合、コードは次の yield 式に向かって実行され、生成された値がgenerator.throw() メソッドの戻り値になります。ジェネレーターがスローされた例外を処理しない場合、例外は呼び出し元のコンテキストにバブルアップします。

generator.close()
を使用すると、ジェネレーターは一時停止された yield 式で GeneratorExit 例外をスローします。ジェネレーターがこの例外を処理しない場合、または StopIteration 例外 (通常は最後まで実行することを指します) をスローした場合、呼び出し元はエラーを報告しません。GeneratorExit 例外を受信した場合、ジェネレーターは値を生成してはなりません。生成しない場合、インタープリターは RuntimeError 例外をスローします。ジェネレーターによってスローされた他の例外は、呼び出し元にバブルアップされます。

1.4 停止インターレーション異常

通常、Python プログラミングを行う場合、一般的に例外に注意を払うことはありませんが、ジェネレーターを学習する場合は、この stopinteration を使用してプログラミングする必要があります。

1.5 ジェネレータを使用した生産者/消費者モデルの実装

1.6 ジェネレーターとコルーチンの関係

2. ジェネレータコルーチンスケジューラ

3.Pythonイベント駆動型プログラミング

4. コルーチンスケジューラの実装

5. Python コルーチンの生態

@asyncio.coroutine と @asyncio.coroutine からの yield
: asyncio モジュールのデコレーター。ジェネレーターをコルーチンとして宣言するために使用されます。
次のコードでは、コルーチン display_date(num,loop) を作成し、キーワード yield from を使用してコルーチン asyncio.sleep(2)() の戻り結果を待機します。そして、待機中の 2 秒間は、asyncio.sleep(2) が結果を返すまで CPU の実行権を放棄します。asyncio.sleep(2) は、実際には 2 秒かかる IO 読み取りおよび書き込み操作をシミュレートします。

import asyncio
import datetime

@asyncio.coroutine  # 声明一个协程
def display_date(num, loop):
    end_time = loop.time() + 10.0
    while True:
        print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= end_time:
            break
        yield from asyncio.sleep(2)  # 阻塞直到协程sleep(2)返回结果
loop = asyncio.get_event_loop()  # 获取一个event_loop
tasks = [display_date(1, loop), display_date(2, loop)]
loop.run_until_complete(asyncio.gather(*tasks))  # "阻塞"直到所有的tasks完成
loop.close()

Python 3.5 では、コルーチンのより直接的なサポートが提供され、async/await キーワードが導入されています。上記のコードは次のように書き直すことができます。@asyncio.coroutine の代わりに async を使用し、yield from の代わりに await を使用すると、コードはより簡潔で読みやすくなります。Python 設計の観点から見ると、async/await によりコルーチンがジェネレーターから独立して存在できるようになり、yield 構文は使用されなくなりました。

import asyncio
import datetime

async def display_date(num, loop):      # 注意这一行的写法
    end_time = loop.time() + 10.0
    while True:
        print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= end_time:
            break
        await asyncio.sleep(2)  # 阻塞直到协程sleep(2)返回结果

loop = asyncio.get_event_loop()  # 获取一个event_loop
tasks = [display_date(1, loop), display_date(2, loop)]
loop.run_until_complete(asyncio.gather(*tasks))  # "阻塞"直到所有的tasks完成
loop.close()

asyncio モジュール
asyncio の使用は、次の 3 つのステップに分けることができます。

イベントループを作成する
ループモードを指定して実行する
ループを閉じる
通常、ループを作成するには asyncio.get_event_loop() メソッドを使用します。

ループを実行するには 2 つの方法があります。1 つは run_until_complete() メソッドを呼び出す方法、もう 1 つは run_forever() メソッドを呼び出す方法です。run_until_complete() には組み込みの add_done_callback コールバック関数があり、run_forever() は add_done_callback() をカスタマイズできます。具体的な違いについては、次の 2 つの例を参照してください。

run_until_complete() メソッドを使用します。

import asyncio

async def func(future):
    await asyncio.sleep(1)
    future.set_result('Future is done!')

if __name__ == '__main__':

    loop = asyncio.get_event_loop()
    future = asyncio.Future()
    asyncio.ensure_future(func(future))
    print(loop.is_running())   # 查看当前状态时循环是否已经启动
    loop.run_until_complete(future)
    print(future.result())
    loop.close()

run_forever() メソッドを使用します。

import asyncio

async def func(future):
    await asyncio.sleep(1)
    future.set_result('Future is done!')

def call_result(future):
    print(future.result())
    loop.stop()

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    future = asyncio.Future()
    asyncio.ensure_future(func(future))
    future.add_done_callback(call_result)        # 注意这行
    try:
        loop.run_forever()
    finally:
        loop.close()

おすすめ

転載: blog.csdn.net/weixin_40293999/article/details/130478060