geventを使ってみよう
geventについて
geventは、Pythonでの単純なシーケンシャルプログラミングを使用して、非同期I/Oと軽量マルチスレッドによって提供されるスケーラビリティを実現するものです。
これは、システムライブラリ libevent、libev(非同期I/O用)およびgreenlets(軽量の協調マルチスレッド)の上に構築されています。
libevent および libevの仕事は、イベントループを処理することです。後述するSocketIOServerは、特定のイベントの発生時に特定の結果を発行できるイベントループです。これは基本的に、SocketIOServerインスタンスがクライアントにメッセージを送信するタイミングを認識する方法であり、特定のイベントの発生時にサーバーからクライアントへのデータのリアルタイムストリーミングです。
"Pythonセミナー並列処理編”で、スレッドベースとプロセスベースでの並行処理を説明しましたが、I/Oバウンドな処理をPythonで並行処理できるようにすることが、greenletsが適合する用途になります。 gevent 1.0より前のバージョンでは、システムライブラリ libevent に基づいています。 gevent 1.0以降では、libev に基づいています。 つまり、geventをビルドするためには次のものが必要になります。
libevent もしくは libev
greenlets
libevent
C言語で実装されたこのライブラリは、非同期イベント通知を提供します。 libevent APIは、ファイル記述子で特定のイベントが発生したとき、またはタイムアウトに達した後にコールバック関数を実行するメカニズムを提供します。 また、シグナルと定期的なタイムアウトによってトリガーされるコールバックもサポートしています。
libev
libeventで行われたいくつかのアーキテクチャ上の問題点を改善する目的で、開発された libeventのフォークバージョンです。
libeventではグローバル変数を使用していたため、マルチスレッド環境でlibeventを安全に使用することが困難。
ウォッチャーの構造は、I/O、時間、およびシグナルハンドラーを1つにまとめているため大きくなる。
httpサーバーやDNSサーバーなどの追加コンポーネントが適切に実装されていない可能性があり、セキュリティの問題が発生する可能性がある。
libevは、グローバル変数を使用せず、すべての関数にループコンテキストを使用することで、これらの問題のいくつかを解決しようとします。 httpおよびdns関連のコンポーネントは完全に削除され、POSIXイベントライブラリのみを実行することに重点が置かれています。
gevent 1.0以降は、libeventの代わりにlibevを使用するように書き直されました。
上記で説明したように、libevはhttpおよびdns機能を処理しないため、c-aresライブラリはlibevent-dnsの代わりに使用されます。 greenletsについて
グリーンレット(greenlets) は、軽量の協調スレッドです。これは、POSIXスレッド(pthread)の従来の理解とは異なります。これは、タスクレット(tasklets)と呼ばれるマイクロスレッドをサポートするCPythonの派生バージョンであるStacklessのスピンオフです。 tasklets(stackless)は疑似並行して(単一または少数のOSレベルのスレッドで)実行され、「チャネル」で交換されるデータと同期されます。 グリーンレットは、これらのタスクレットマイクロスレッドと比較してより原始的であり、コルーチン(Coroutine: 協調ルーチン) としてより正確に記述されます。 つまり、グレーンレット には タスクレットのような暗黙のスケジューリングがなく、コードの実行時間を正確に制御できます。
グリーンレットについてもう少し詳しく説明します。
グリーンレットは、プロセス内でスケジュールおよび管理される軽量のスレッドのような構造です。これらは、スレッドによって使用されるスタックの部分参照です。 POSIXスレッド(pthreads)と比較すると、事前に割り当てられたスタックはなく、グリーンレットで実際に使用されているスタックの数だけがあります。
Pythonでは、geventパッケージを介してgreenletsを実装し、Pythonの標準ライブラリ threadingモジュールを介してpthreadを実装します。
グリーンスレッド(greenlets)とPOSIXスレッド(pthread)はどちらも、プログラムのマルチスレッド実行をサポートするメカニズムです。
POSIXスレッドは、オペレーティングシステムのネイティブ機能を使用して、マルチスレッドプロセスを管理します。 pthreadを実行すると、カーネルはプロセスを構成するさまざまなスレッドをスケジュールして管理します。
グリーンスレッドは、ネイティブオペレーティングシステムの機能に依存することなく、マルチスレッド環境をエミュレートします。グリーンスレッドは、スレッドを管理およびスケジュールするユーザースペースでコードを実行します。
greenlets とpthreadの主な違いは、次のように要約できます。
pthreads
スレッドは先制的にスレッドを切り替えることができ、いつでも実行中のスレッドから非実行中のスレッドに制御を切り替えることができます
マルチコアマシンでは、pthreadは複数のスレッドを実行できます。 ただし、PythonのGILは並列処理を阻害するため、同時実行はI / Oバウンドプログラムに対してのみ有効です。
マルチスレッドコードを実装すると、競合状態が発生する可能性があります。 ロックを使用して排他制御を管理し、競合状態を回避するようにします。
greenlets
greenletsは、制御がスレッドによって明示的に放棄された場合(yield()またはwait()を使用した場合)、またはスレッドが読み取りや書き込みなどのI/Oブロッキング操作を実行した場合にのみ切り替わります。
greenletsは単一のCPUでのみ実行でき、I/Oバウンドプログラムで有効に機能します。
greenletsの場合、2つの制御スレッドが同じ共有メモリに同時にアクセスする可能性はないため、競合状態は発生しません。
geventのインストール
gevent は次のようにインストールします。
code: bash
$ pip install gevent
これで依存モジュールの greenlet も合わせてインストールされます。
greenletを単独で使用する場合は、は次のようにインストールします。
code: bash
$ pip install greenlet
greenletの使用方法
greenlet の実行の性質を説明するために、簡単な例を次に示します。
code: greenlet_sample.py
from greenlet import greenlet
def test1():
print(12)
gr2.switch()
print(34)
def test2():
print(56)
gr1.switch()
print(78)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
code: bash
$ python greenlet_sample.py
12
56
34
geventのAPIデザイン
geventのインターフェースは、Python標準ライブラリによって設定された規則に従っています。
gevent.event.Eventは、Pythonの組み込みモジュールthreading.Eventおよびmultiprocessing.Eventと同じインターフェースと同じセマンティクスを持っています。
wait()は例外を発生させません
get()は、例外を発生させたり、値を返したりすることができます
join()はwait()に似ていますが、実行ユニット用です
このした一貫性のあるコードインターフェイスを持つことで、プログラマーは効率的な方法でコードを読むことができ、動作を理解することができます。
genvetとPythonの他の拡張モジュール
ある種のトランザクションにI/Oが含まれる場合、書き込み確認応答(または他の種類のI/Oブロック)を待ってグリーンレットが切り替わる可能性があるため、トランザクションを明示的にロックする必要があります。 コードが古いブロッキングI/Oスタイルに戻ると、アプリケーション全体が失敗します。 これを防ぐには、Python の標準ライブラリsocketモジュールを利用する拡張機能のみを使用するようにしてください。
gevent のモンキーパッチ
モンキーパッチ(Monkey Patch)は、元のソースコードを変更せずに、動的言語のランタイムコードを拡張または変更する方法のことです。プログラミング手法としてのモンキーパッチは非常に強力ですが、デバッグが困難なコードが悪用される可能性があります。
補足説明
Jeff Atwoodは、これらの問題についての良い投稿を書いています。 モンキーパッチを行うライブラリを使用する場合は常に、ソースコードとドキュメントを完全に読み、そのライブラリのモンキーパッチが標準のソースコード、モジュール、およびライブラリにどのように影響するかを理解することが重要です。
geventの最も重要な機能の1つはモンキーパッチです。そのため、モンキーパッチが実際に何をするのか を理解する必要があります。 このモジュールのモンキーパッチに関する関数は、geventパッケージの互換性のある協調的なコードで標準ライブラリの一部にパッチを適用します。
個々のモジュールにパッチを適用するには、対応するpatch_ *関数を呼び出します。たとえば、socketモジュールにのみパッチを適用するには、patch_socket()を呼び出します。すべてのデフォルトモジュールにパッチを適用するには、gevent.monkey.patch_all()を呼び出します。
Monkeyクラスは、スレッドとスレッドにパッチを適用して、グリーンレットベースにすることもできます。したがって、thread.start_new_thread() は代わりに新しいグリーンレットを開始し、threading.localはグリーンレットローカルストレージになります。
次のコードは正常に動作します。
code: python
import gevent.monkey;gevent.monkey.patch_thread()
import threading
しかし、次の例は失敗してしまいます。
code: python
import threading
import gevent.monkey; gevent.monkey.patch_thread()
threadingモジュールがインポートされると、メインスレッドIDがモジュールレベルのスレッドディクショナリのキーとして使用されます。 プログラムが終了すると、threadingモジュールは(現在のスレッドIDを使用して)ディクショナリからスレッドインスタンスを取得してクリーンアップを実行しようとします。
簡単に言えば、モンキーパッチのgeventの順序が重要です。 特にコードがある時点でスレッドを使用している場合は、Pythonコードを実行する前に、必ず最初にモンキーパッチを実行してください。 ロギングモジュールもスレッドを使用するため、アプリケーションをロギングするときは、最初にモンキーパッチを使用することに注意してください。
Webサーバーとのgevent
ほとんどのWebアプリケーションは、http経由でリクエストを受け入れます。 geventを使用すると、PythonのソケットAPIをシームレスに操作できるため、ブロッキング呼び出しは発生しません。ただし、Pythonソケットを回避する可能性のあるC言語拡張のモジュールを追加する場合は注意が必要です。
データベースを使用したgevent
Pythonアプリケーションは通常、Webサーバーとデータベースの間にあります。 geventを利用したPythonアプリケーションが、Pythonソケットを回避するC言語拡張のコードやモジュール依存の影響を受けないことを確認したので、適切なデータベースドライバーを使用していることを確認してください。
gevent を利用したPython アプリで動作するデータベースドライバーは次のとおりです。
mysql-connector
pymongo
redis-py
psycopg
MySQLで標準的なMySQLdbドライバーはC言語ベースであるため使用できません。
データベース接続をどのように設計するかは、httpインターフェイスがどのように機能するかによって異なります。たとえば、greenlet-poolを使用すると、リクエストごとに新しいグリーンレットが生成されます。データベース側では、redis-pyの場合、すべてのredis.Connectionインスタンスに1つのソケットが接続されています。 redis-clientは、これらの接続のプールを使用します。すべてのコマンドはプールから接続を取得し、後でそれを解放します。グリーンレットごとに1つの接続を作成する余裕がないため、これはgeventで使用するのに適したデザインパターンです。データベースはスレッドで確立されたすべての接続を処理することが多いため、データベース側のリソースがすぐに不足する可能性があります。
一方、単一の接続を使用すると、大きなボトルネックが発生します。接続数が制限されている接続プールはパフォーマンスを低下させる可能性があるため、本番アプリケーションでは、アプリの使用パターンが変化するにつれて、接続制限を慎重に決定する必要があります。
pymongoは、ライフタイム全体を通じて1つのグリーンレットに対して1つの接続を使用することを保証できるため、読み取りと書き込みの一貫性が保たれます。
I/O操作を伴うgevent
GILのため、Pythonスレッドは並列ではありません。 geventのグリーンレット使うだけで、並列処理を実現するわけではありません。特定のプロセスで実行されるグリーンレットは常に1つだけです。このため、CPUバウンドのアプリは、geventやPythonのthreadingを使用してもパフォーマンスが向上しません。
geventは、I/Oのボトルネックを解決する場合にのみ役立ちます。geventを用いたPythonアプリケーションはhttp接続、データベース、そしておそらくキャッシュやメッセージングサーバーの間に置かれるはずで、その場合は、geventは便利に機能します。
ファイルI/O操作ではgeventを使えない
geventは通常のファイルの読み取り/書き込み(つまり、ファイルI/O)を適切に処理しません。
POSIXによると:
通常のファイルに関連付けられたファイル記述子は、読み取り準備完了、書き込み準備完了、およびエラー状態に対して常にtrueを選択する必要があります。
Linuxのmanページには次のように書かれています。
多くのファイルシステムとディスクは、O_NONBLOCKの実装が不要とされるほど
高速であると見なされていました。
そのため、O_NONBLOCKはファイルやディスクで使用できない場合があります。
libev のドキュメントによると:
(抜粋)...カーネルがデータの有無と量を認識するとすぐに準備通知を受け取ります。開いているファイルの場合は準備通知を即座に受け取り、読み取りや書き込みは、ディスクI/Oでブロックされます。つまり、ファイルI/Oは必ずブロックされるため、非同期処理ではうまく機能しません。要求を処理する前に必要なすべてのファイルを事前にロードするか、別のプロセスでファイルI/Oを実行するようにします(パイプはノンブロックI/Oをサポートします)。
gevent のサンプル
以下のコードは、geventのI/Oパフォーマンスの利点を利用する簡単な例です。 典型的なWebのリクエスト/レスポンスサイクルでは、次のようなジョブを同時実行したい場合があります。
特定のデータベースからデータソースを取得したり、
外部のアプリケーションにGETリクエストを送信したり、
社内のアプリケーションからAPIへの要求にレスポンスをJSONで返したり、
SMTP接続をインスタンス化してメールを送信したり、
などなど...
もちろん、これらのタスクを1つずつ順番に実行することもできます。 しかし、これらを同時に実行したい場合は gevent で記述することができます。
上記のI/OジョブのいずれかでI/Oボトルネックが発生すると、タスクが切り替わります。
code: python
def handle_view(request):
jobs = []
jobs.append(gevent.spawn(orm_call, 'Andy'))
jobs.append(gevent.spawn(call_facebook_graph_api, 14213))
jobs.append(gevent.spawn(email, 'me@mysite.com'))
gevent.joinall()
gevent によるグリーンレットの生成
geventは、ベースのgreenletライブラリのラッパークラスGreenletを提供します
code: python
import gevent
from gevent import Greenlet
Greenletクラスによる実装
code: pytohn
import gevent
from gevent import Greenlet
def foo(message, n):
"""
初期化のために、各スレッドには引数 message と n が渡される
"""
gevent.sleep(n)
print(message)
# 名前付き関数fooを実行している新しいGreenletインスタンスを初期化
thread1 = Greenlet.spawn(foo, "Hello", 1)
# 渡された引数を使用して、名前付き関数fooから
# 新しいGreenletを作成および実行するためのラッパー
thread2 = gevent.spawn(foo, "I live!", 2)
# Lambda式
thread3 = gevent.spawn(lambda x: (x+1), 2)
# すべてのスレッドが完了するまでブロック
gevent.joinall(threads)
Greenletクラスを継承したクラスを作成して、_run()メソッドをオーバーライドした実装
code: python
import gevent
from gevent import Greenlet
class MyGreenlet(Greenlet):
def __init__(self, message, n):
Greenlet.__init__(self)
self.message = message
self.n = n
def _run(self):
print(self.message)
gevent.sleep(self.n)
g = MyGreenlet("Hi there!", 3)
g.start()
g.join()
まとめ
geventは、小さい処理(minimum)のスレッド化に関連するオーバーヘッドを削減するのに役立ちます。 (グリーンレット)
geventは、非同期のイベントベースのI/Oを使用することにより、I/O中のリソースの浪費を回避するのに役立ちます。
geventは、Webサーバー、データベース、キャッシュ、およびメッセージングフレームワークを使用した同時実行の実装に非常に適しています。これらは、I/Oバウンド操作であるためです。
I/Oパフォーマンス向上の例外は、ファイルI/Oです。 これに対処するには、ファイルを事前にロードするか、別のプロセスでファイルI/Oを実行するようにします
geventは、マルチコアCPUバウンドプログラムのソリューションではありません。 これに対処するには、CPUを集中的に使用するコードをキューまたは別のプログラムに任せて、メッセージキューから結果を返すようにします。
参考