本稿では、オペレーティング・システムの原則とコードの実践を組み合わせて、以下のことを説明します:
- プロセス、スレッド、コプロセスとは何ですか?
- 両者の関係は?
- なぜPythonのマルチスレッドは擬似マルチスレッドなのですか?
- さまざまなアプリケーション・シナリオに対して、どのように技術的ソリューションを選べばよいのでしょうか?
- ...
プロセスとは何ですか?
プロセス - オペレーティング・システムが提供する抽象的な概念で、リソースの割り当てとスケジューリングのためのシステムの基本単位であり、オペレーティング・システムの構造の基礎。プログラムは、命令、データ、およびそれらの組織の記述であり、プロセスはプログラムの実体です。プログラム自体にはライフサイクルはなく、ディスク上に存在するいくつかの命令にすぎません。
プログラムの実行が必要になると、OSはコードとすべての静的データをメモリとプロセスのアドレス空間に記録し、スタック(ローカル変数、関数パラメータ、リターンアドレス)の作成と初期化、ヒープメモリの確保、IO関連タスクを行い、前準備が完了してプログラムが開始されると、OSはCPUの制御を新しく作成されたプロセスに移し、プロセスの実行を開始します。
オペレーティングシステムはPCB(Processing Control Block)を通じてプロセスを制御・管理します。PCBは通常、システムメモリ占有領域内の連続した記憶領域で、プロセスの状況を記述し、プロセスの動作を制御するためにオペレーティングシステムが必要とするすべての情報を格納します。
プロセスには、初期状態、準備完了状態、待機状態、実行状態、終了状態の少なくとも5つの基本状態があります。
- 初期状態:プロセスは作成されたばかりで、他のプロセスがCPUリソースを占有しているため、実行することができません。
- レディ状態:レディ状態のものだけが実行状態にスケジューリング可能
- 待機状態:プロセスが何かの完了を待つ状態
- 実行状態:実行状態にあるプロセスは、任意の時点で1つだけです。
- 停止状態:プロセスが終了
プロセス間の切り替え
マルチコアでもシングルコアでも、CPUは複数のプロセスを同時に実行しているように見えます。
現在のプロセスのコンテキストが保存され、新しいプロセスのコンテキストが復元された後、CPU制御が新しいプロセスに移され、停止したところから開始されます。このように、プロセスは複数のプロセスで共有されるCPUを交代で使用し、あるプロセスを停止して別のプロセスに切り替えるタイミングを決定するために、ある種のスケジューリングアルゴリズムを使用します。
- つのプロセスを持つシングルコアCPUの場合
プロセスはコンテキストを切り替え、特定のスケジューリングメカニズムに従って、またI/O割り込みなどに遭遇したときにCPUリソースを交代で使用します。
- デュアルコアCPUとデュアルプロセスの場合
各プロセスは1つのCPUコア・リソースを排他的に占有し、I/O要求を処理する際にCPUはブロッキング状態になります。
プロセス間データ共有
システム内のプロセスは、CPUとメイン・メモリのリソースを他のプロセスと共有します。メイン・メモリをより適切に管理するために、オペレーティング・システムはメイン・メモリを仮想メモリとして抽象化します。これは、各プロセスがメインメモリを排他的に使用しているかのように錯覚させる抽象化でもあります。
仮想メモリは主に3つの機能を提供します:
- メインメモリーをディスクに保存されたキャッシュと考え、アクティブな領域だけをメインメモリーに残し、必要に応じてディスクとメインメモリーの間でデータを行き来させることで、メインメモリーをより効率的に使用します。
- 各プロセスに一貫したアドレス空間を提供することでメモリ管理を簡素化
- 各プロセスのアドレス空間が他のプロセスによって破壊されないように保護
プロセスはそれぞれ排他的な仮想アドレス空間を持っているため、CPUはアドレス変換によって仮想アドレスを実際の物理アドレスに変換し、各プロセスは自分のアドレス空間にしかアクセスできません。そのため、プロセス間でデータを共有することは、他の仕組みを利用しなければ不可能です。
- pythonの複数プロセスの例を見てみましょう:
import multiprocessing
import threading
import time
n = 0
def count(num):
global n
for i in range():
n += i
print("Process {0}:n={1},id(n)={2}".format(num, n, id(n)))
if __name__ == '__main__':
start_time = time.time()
process = list()
for i in range(5):
p = multiprocessing.Process(target=count, args=(i,)) # マルチプロセスをテストする
# p = threading.Thread(target=count, args=(i,)) # マルチスレッドをテストする
process.append(p)
for p in process:
p.start()
for p in process:
p.join()
print("Main:n={0},id(n)={1}".format(n, id(n)))
end_time = time.time()
print("Total time:{0}".format(end_time - start_time))
- 結局
Process 1:n=,id(n)=72440
Process 0:n=,id(n)=46064
Process 2:n=,id(n)=72400
Process 4:n=,id(n)=18960
Process 3:n=,id(n)=69320
Main:n=0,id(n)=
Total time:0.
変数nは、プロセスp{0,1,2,3,4}とマスタープロセスの両方で一意なアドレス空間を持ちます。
スレッドとは何ですか?
スレッド(オペレーティングシステムが提供する抽象化でもあります)とは、プログラムの実行において逐次的に制御される単一のプロセスであり、プログラムの実行フローの最小単位であり、プロセッサのスケジューリングと割り当ての基本単位です。プロセスは 1 つ以上のスレッドを持つことができ、同じプロセス内の複数のスレッドは、仮想アドレス空間、ファイル記述子、信号処理など、プロセス内のすべてのシステムリソースを共有します。しかし、同じプロセス内の複数のスレッドは、独自のコールスタックとスレッドローカルストレージを持ちます。
システムは PCB を使用してプロセスの制御と管理を行います。同様に、システムはスレッド制御ブロック TCB をスレッドに割り当て、スレッドの制御と管理に使用されるすべての情報をスレッドの制御ブロックに記録します:
- スレッド識別子
- レジスタ
- スレッドの実行状態
- 優先順位
- スレッド専有ストレージ
- 信号遮蔽
プロセスと同様に、スレッドにも初期状態、準備完了状態、待機状態、実行状態、終了状態の少なくとも5つの状態があります。
スレッド間の切り替えには、プロセスと同じようにコンテキストの切り替えが必要です。
プロセスとスレッドには多くの共通点がありますが、具体的に何が違うのでしょうか?
プロセス VS スレッド
- プロセスは、リソースの割り当てとスケジューリングのための独立した単位です。プロセスは完全な仮想アドレス空間を持ち、プロセス切り替えが発生すると、異なるプロセスは異なる仮想アドレス空間を持ちます。また、同じプロセスの複数のスレッドが同じアドレス空間を共有します。
- スレッドはCPUスケジューリングの基本単位で、1つのプロセスには複数のスレッドが含まれます。
- スレッドはプロセスよりも小さく、基本的にシステム・リソースを所有しません。スレッドの作成と破棄にかかる時間は、プロセスよりもはるかに短くなります。
- スレッドは互いにアドレス空間を共有できるので、同期とミューテックス操作を考慮する必要があります。
- スレッドの偶発的な終了はプロセス全体の正常な動作に影響を与えますが、プロセスの偶発的な終了は他のプロセスの動作に影響を与えません。したがって、マルチプロセス・プログラムはより安全です。
つまり、マルチプロセスプログラムは安全性が高く、プロセス切り替えのオーバーヘッドが大きく、効率が低いです。マルチスレッドプログラムは保守性が高く、スレッド切り替えのオーバーヘッドが小さく、効率が高いです。(Pythonのマルチスレッドは擬似マルチスレッドです。詳細は後述します)
コンカレント・プログラミングとは
コンカチネーションはスレッドよりも軽量な存在で、オペレーティング・システムのカーネルによって管理される代わりに、コンカチネーションは完全にプログラムによって制御されます。コンカチネーションとスレッドやプロセスとの関係は、次の図のようになります。
- 連結はサブルーチンに例えることができますが、実行中にサブルーチンが内部的に中断され、他のサブルーチンの実行に切り替わり、適切なタイミングで戻って実行を継続することができます。同時実行プログラム間の切り替えには、システム・コールやブロッキング・コールを使用する必要はありません。
- 連結は1つのスレッドのみで実行され、サブルーチン間の切り替えはユーザーステート上で行われます。さらに、スレッドのブロッキング状態はオペレーティングシステムのカーネルによって行われ、カーネルステートで発生します。
- 並行プロセスでは同時に書き込み変数が衝突することはないため、ミューテックス・ロックやセマフォなど、クリティカル・ブロックを保護するために使用されるシンクロニシティ・プリミティブは必要なく、オペレーティング・システムからのサポートも必要ありません。
IOのブロッキングと同時並行シナリオの数が多いの必要性のために同時、IOのブロッキングが発生したときに、同時スケジューラのスケジューリングによって、データストリームの降伏オフによって、スタック上の現在のデータを記録し、スレッドの完了直後に同時スタックを復元するためにブロッキングし、このスレッド上で実行するためにブロッキングの結果を置きます。
以下では、Pythonでプロセス、スレッド、並行スレッドをさまざまなアプリケーションシナリオで使用する方法を分析します。
どう選ぶ?
シナリオ別に3つの違いを比較するには、まずpythonのマルチスレッドを導入する必要があります。
では、なぜPythonのマルチスレッドは「疑似」マルチスレッドとみなされるのでしょうか?
p = multiprocessing.Process(target=count, args=(i,))
p = threading.Thread(target=count, args=(i,))
上記のマルチプロセッシングの例を、他のコードは変更せずに、次のように置き換えます:
コードの冗長性と記事の長さを減らすために、命名と印刷の不規則性は無視されるべきです。
Process 0:n=,id(n)=85600
Process 2:n=3,id(n)=85600
Process 1:n=7,id(n)=85600
Process 4:n=9,id(n)=72912
Process 3:n=2,id(n)=85600
Main:n=9,id(n)=72912
Total time:0.
- nはグローバル変数で、Mainのプリントアウトはスレッドと等しく、スレッドが互いにデータを共有していることを証明しています。
しかし、なぜマルチスレッドの方がマルチプロセッシングよりも実行に時間がかかるのでしょうか?これは上記の事実と深刻な矛盾があります。そこでCpythonのGIL(Global Interpreter Lock)の登場です。
GILとは
GILはPythonの元々の設計に由来し、データセキュリティのために決定されました。スレッドが実行したい場合、最初にGILを取得しなければなりません。GILは「パス」と考えることができ、PythonのプロセスにはGILが1つしかないので、パスを取得しないスレッドはCPUに入ることができません。
Cpythonインタープリタは、メモリ管理で参照カウントを使い、参照カウントがゼロになるとオブジェクトをゴミとしてリサイクルします。このようなシナリオを想像してみてください:
あるプロセスには、オブジェクト a を参照するスレッド 0 とスレッド 1 の 2 つのスレッドがあります。2 つのスレッドが同時に a を参照すると、オブジェクト a の参照カウンタが同時に変更されるため、参照カウントが実体参照よりも少なくなり、ごみ収集時にメモリ例外エラーが発生します。そのため、オブジェクトの参照カウントの正しさと安全性を保証するためにグローバルロックが必要です。
シングルコアであろうとマルチコアであろうと、プロセスは一度に1つのスレッドしか実行できないため、Pythonのマルチスレッド性能はマルチコアCPUではあまり良くありません。
では、Pythonで並行処理が必要な場合、マルチプロセッシングを使えばすべてうまくいくというのは本当でしょうか?実はそうではありません。ソフトウェア工学では「特効薬はない」という有名な言葉があります!
いつ使うのですか?
一般的な適用シナリオは3つ以上ありません:
- CPU集約型:計算やデータ処理に大量のCPUを必要とするプログラム;
- I/Oインテンシブ:ソケットデータ転送やネットワークでの読み込みなど、頻繁にI/O操作を必要とするプログラム;
- CPU集約型+I/O集約型:上記両方の組み合わせ
CPUを多用するケースは、上記のPythonのマルチプロセシングとスレッディングの例と比較することができます。
主なI/O集約シナリオを以下に説明します。オペレーティング・システムがI/Oデバイスと対話するために使用する最も一般的なソリューションは、DMAです。
DMAとは
DMA(Direct Memory Access)とは、CPUが中間処理に介入することなく、メモリからデバイスへのデータ転送を調整するシステム内の特別なデバイスです。
ファイルの書き込みを例にとってみましょう:
- プロセスp1がディスクファイルへのデータ書き込み要求を発行
- CPUは書き込み要求を処理し、DMAエンジンにメモリ内のデータの場所、書き込むデータのサイズ、ターゲットデバイスを伝えるようにプログラムされています。
- CPUは他のプロセスp2からの要求を処理し、DMAはメモリ・データをデバイスに書き込む役割を担います。
- DMAがデータ転送を完了し、CPUに割り込み
- CPUはp2コンテキストからp1に切り替え、p1の実行を継続します。
Pythonのマルチスレッド性能
- スレッド0が最初に実行され、スレッド1は待機します。
- スレッド0はI/O要求を受け取り、要求をDMAに転送し、DMAは要求を実行します。
- スレッド1がCPUリソースを占有し、実行を継続
- CPUがDMAからの割り込み要求を受信、スレッド0に切り替えて実行継続
プロセスの実行モードと同様に、GILがもたらす欠点を補い、スレッドのオーバーヘッドはプロセスのそれよりもはるかに小さいため、マルチスレッドはIO集約的なシナリオでより高いパフォーマンスを発揮します。
I/O集中型のシナリオでは、実践が真実をテストする唯一の基準です。
テスト
- 結果
import multiprocessing
import threading
import time
def count(num):
time.sleep(1) ## IOオペレーションをシミュレートする
print("Process {0} End".format(num))
if __name__ == '__main__':
start_time = time.time()
process = list()
for i in range(5):
p = multiprocessing.Process(target=count, args=(i,))
# p = threading.Thread(target=count, args=(i,))
process.append(p)
for p in process:
p.start()
for p in process:
p.join()
end_time = time.time()
print("Total time:{0}".format(end_time - start_time))
- 結局
##
Process 0 End
Process 3 End
Process 4 End
Process 2 End
Process 1 End
Total time:1.52246
##
Process 0 End
Process 4 End
Process 3 End
Process 1 End
Process 2 End
Total time:1.07373
- マルチスレッド実行はマルチプロセッシングより効率的
上述したように、I/Oを多用するプログラムでは、連結の実行はプログラム自身によって制御されるため効率的であり、スレッドの作成や切り替えのオーバーヘッドを節約できます。
Pythonのasyncio同時実行コードベースを依存関係として使用し、同時実行の作成と使用のためにasync/await構文を使用します。
- プログラムコード
import time
import asyncio
async def coroutine():
await asyncio.sleep(1) ## IOオペレーションをシミュレートする
if __name__ == "__main__":
start_time = time.time()
loop = asyncio.get_event_loop()
tasks = []
for i in range(5):
task = loop.create_task(coroutine())
tasks.append(task)
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
end_time = time.time()
print("total time:", end_time - start_time)
- 結局
total time: 1.08252
- 並行スレッドはマルチスレッドよりも効率的です。
まとめ
この記事では、プロセス、スレッド、並行処理とそれらの関係について、オペレーティングシステムの原則とコードの実践から説明します。さらに、Pythonの実践におけるさまざまなシナリオに対応する解決策をどのように選択するかをまとめ、整理しています:
- CPU集約型:マルチプロセス
- IO集約型:マルチスレッド
- CPU集約型とIO集約型:マルチプロセシング+連結
気軽にフォローして、一緒に学び、向上していきましょう。