非同期処理について
非同期処理
非同期処理を説明するにあたって、まず言葉や概念の理解を深めることが大事だと考えています。とくに並列処理や非同期処理に関しては、よく似た概念の言葉がしばしば使われるので、混乱することが多いからです。
まず、Concurrent と Parallel はどちらも一般的には並列と訳されますが、英語の概念がうまく日本語にできていないため、曖昧になりやすいので整理しておきましょう。
Conucurrent - 複数の処理を並行に実行する。
Parallel - 複数の処理を同時に並列で処理をする
Concurrentについて
Concurrentの英語の概念に近い日本語では、並行処理が当てはまるでしょう。
Concurrent の辞書の定義では、同時発生のことです。 Pythonでは、同時に発生する処理を行うものに異なる名前(スレッド、タスク、プロセス)が使われます。同じ概念に異なる単語を使用されるため混乱しやすいのですが、スレッド、タスク、およびプロセスは、高レベルから見ると同じ意味になります。それらを、詳しく掘り下げていくと、すべてわずかに異なるものが見えてきます。
threadingとasyncioは両方とも単一のプロセッサで実行されるため、一度に1つしか実行されません。これらは、同時に異なる処理を実行することはできませんが、プロセス全体をスピードアップするために切り替える巧妙な方法があるため、これを並行処理(Concurrent)と呼びます。
スレッドまたはタスクが切り替わる方法が、threading と asyncio では大きく違ってきます。threadingでは、オペレーティングシステム(OS)は実際に各スレッドを認識しいて、「いつでもスレッド切り替える」ことができます。つまり、OS側の都合で動作中のスレッドを中断して別のスレッドの実行を開始します。これを、「OSがスレッドをプリエンプトして切り替えを行うことができる」と言い、非協調的マルチタスク(プリエンプティブマルチタスク: preemptive multitasking)と呼ばれます。
プリエンプティブマルチタスクは、スレッド内のコードが切り替えを行うためにプログラマが何もする必要がないという点で便利です。しかしこれは、その「いつでもスレッドが切り替わる」ことを、プログラマが把握できないため難しい場合もあります。この切り替えは、ただ単に変数をセットするだけのPythonの文であっても発生する可能性があります。
これに対して、asyncioはコルーチン(Coroutine:協調ルーチン) を使ってマルチタスクを行います。コルーチンタスクは、スイッチアウトの準備ができたときにアナウンスすることによって協調させる必要があります。つまり、これを実現するためには、タスクのコードを少し変更する必要があるわけです。この余分なコードを記述することはデメリットだと思うかもしれませんが、事前にタスクがどこで切り替わるかをプログラマが常に知っていることは非常に重要で大きなメリットとなります。
Parallelについて
multiprocessing は文字通りマルチプロセスで同時に実行するため、少し注意する必要があります。multiprocessing を使用すると、Pythonは新しいプロセスを作成できるようになります。ここでのプロセスは、ほぼ完全に異なるプログラムと考えることができますが、技術的には、リソースにメモリやファイルハンドルなどが含まれるリソースの集まりとして定義されます。multiprocessing を使用するときに注意が必要なことは、それぞれのプロセスが独自のPythonインタープリターで実行されるということです。つまり、生成したプロセスの数だけPythonインタプリターが存在することになります。
これらは異なるプロセスであるため、マルチプロセッシングプログラムではそれぞれ異なるCPUで実行されることになります。別のCPUで実行するということは、確実に同時実行できるということです。ただし、それぞれのプロセスは別のメモリ空間にあることから、データやオブジェクト参照などで、いくつか複雑な手続きが必要になりますが、Pythonでは少しコードを追加することで対応するすることができます。
非同期処理という概念
非同期処理を簡単に言うと、前のタスクが完了するのを待たずに、さまざまなタスクの実行を開始して処理を行うことを言います。
非同期処理プログラミングは、従来の逐次処理とはコーディングが少し複雑で難しくなりますが、遥かに効率的に処理することができます。
非同期処理が必要になってくる問題
例えば、HTTPリクエストに対して何かを処理するような場合を考えてみましょう。
リクエストの処理が完了してから次のリクエストを処理するのではなく、非同期コルーチンを使用してリクエストを送信し、リクエストの処理が完了するのを待っている間に、キューに待機している他のリクエストを処理することができます。
正常に動作させるためには設計に配慮が必要になりますが、少ないリソースでより多くの作業を処理できるようになります。
マルチスレッドではメモリをそれぞれのスレッドで共有しています。そのため、ロックを必要とする場合があり、せっかくの並列性が失われてしまうことがあります。
また、マルチスレッドでリクエストを処理するようなとき、リクエスとごとにスレッドを生成するような場合は、C10K問題が発生することになります。
C10K問題とは、簡単にいうとリクエストが多すぎるとサーバーで処理できなくなるというもので、概ね Client の数が 10Kilo (1万)個あたりで具現化することからC10K問題と言われます。
非同期処理では1スレッドで複数リクエストを処理するため、こうした問題を回避することができます。
非同期処理に向いている問題は次の特徴があります。
処理が大量に発生する
処理を完了させる順序を問わない(これが重要)
イベント内に同じ処理(コルーチン)が複数ある
非同期処理での注意点
非同期処理はスレッドの扱い方が、マルチスレッドとは異なります。このため、マルチスレッドと非同期処理とを共存させることはできません。
マルチプロセスと非同期処理は共存することができます。
非同期処理を行う代表的なモジュール
Python で使用される非同期処理のモジュールのうちで代表的なものを次にあげます。
gevent、eventlet
スレッドはハードウェアではなくアプリケーションレベルで制御
スレッドのように見えるが、CPUコンテキストスイッチは起きない
通常のスレッドプログラミングの問題点は残る
コンテキストスイッチ
https://gyazo.com/aa6829f99c6b2efb8b9b0119e0eccea9
asyncio
CPUコンテキストスイッチングは起きない
イベントループを使用して、アプリケーションレベルでコルーチンを切り替える
asyncio は1度に1つのコルーチンだけを実行する
競合が発生しない
デッドロックにならない
アプリケーションでロックする必要がない
コルーチンはスレッドで実行されるので余分なソケットやメモリは不要
リソースが欠乏することがない
Python3.4 から導入された asyncio は非常に進歩、発展が激しいので注意が必要です。
非同期処理を行うのであれば、Python3.7以降の利用を推奨します。