pythonスレッド、GILとctypes

12654 ワード

原文の住所:http://zhuoqiang.me/python-thread-gil-and-ctypes.html
GILとPythonスレッドの葛藤
GILとは何ですか?私たちのpythonプログラムにはどのような影響がありますか?まず問題を見に来ます。次のpythonプログラムを実行して、CPUの占有率はいくらですか?
#         ,  :)
def dead_loop():
    while True:
        pass

dead_loop()
答えは何ですか?100%CPUを占有しますか?それはシングルコアです超スレッドがないアンティークCPUです。私のデュアルコアCPUでは、このデッドサイクルは私の核を食べてしまう作業負荷、つまりCPU 50%だけを占めます。デュアルコアマシンでCPUを100%占有するにはどうすればいいですか?答えは簡単に思いつきます。二つのスレッドを使えばいいです。スレッドはCPUの演算リソースを同時に共有しているのではないですか?残念ですが、答えは正しいですが、簡単にはできません。次のプログラムはメインライン以外にもう一つのデッドループのスレッドを立てました。
import threading

def dead_loop():
    while True:
        pass

#          
t = threading.Thread(target=dead_loop)
t.start()

#          
dead_loop()

t.join()
道理によって、二つの核のCPU資源を占用できるはずですが、実際の運行状況は何の変化もないです。それとも50%のCPUしか占めていません。これはなぜですか?pythonスレッドはオペレーティングシステムの元のスレッドではないですか?system monitorを開けてみると、これが50%を占めるpythonプロセスは確かに二つのスレッドが走っています。この2つの死循環スレッドはどうしてデュアルコアCPU資源をいっぱい占められないのですか?実は黒幕はGILです。
GILの迷思:痛くて楽しいです。
GILの全行程は Global Interpreter Lock ,全体のインタプリタロックという意味です。Python言語の主流であるCPythonでは、GILは本物のグローバルスレッドロックであり、インタプリタがどのPythonコードを実行するかを説明する際には、このロックを先に獲得しなければなりません。I/O操作に遭遇した時にこのロックを解除します。純計算のプログラムであれば、I/O操作がなく、このロックを解除して、他のスレッドに実行させることができます。 sys.setch eckinterval 調整します。したがって、CPythonのスレッドライブラリは直接にオペレーティングシステムの元のスレッドをカプセル化しているが、CPythonプロセスは全体として、同じ時間にGILを獲得したスレッドが一つしかないため、他のスレッドは全部待ち状態でGILのリリースを待っている。これは、上記の実験結果を説明します。2つのデッドサイクルのスレッドがあり、しかも2つの物理CPUカーネルがありますが、GILの制限のため、2つのスレッドはタイムスイッチをしているだけで、全体のCPU占有率はまだ50%以下です。
pythonは力がなさそうですね。GILは直接CPythonが物理的に多核の性能を利用して計算を加速することができないことを招きます。なぜこのようなデザインがありますか?やはり歴史的に問題を残すべきだと思います。マルチコアCPUは1990年代にはSFに属していました。Gido van Rossumがpythonを作った時、彼の言語がいつか1000+の核を使うかもしれないCPUの上にも使われています。グローバルロックがマルチスレッドの安全を解決するのはその時代で一番簡単な経済設計であるはずです。シンプルで需要を満たすことができます。それは適切なデザインです。ハードウエアの発展は速すぎて、ムーアの法則はソフトウェア業の配当金にこんなに早く頭打ちになります。わずか20年で、コード労働者はCPUをアップグレードするだけで、古いソフトウェアを速く走ることができるとは期待できません。核時代には、プログラムの無料昼食がなくなりました。プログラムが核ごとの演算性能を同時に絞り込むことができないと、淘汰されるという意味です。ソフトウェアに対しても、言語に対しても同じです。Pythonの対策は?
Pythonの対応は簡単で、変化に対応しています。最新のpython 3には依然としてGILがあります。なぜ取り除かないのか、原因は以下の通りです。
神功を磨きたいと思い、刀を振るって宮を出る:CPythonのGILは本来、全世界の解釈器と環境状態変数を保護するために使われたのです。GILを落とすには、より細かい錠が必要です。あるいはLock-Freeアルゴリズムを採用します。いずれにしても、マルチスレッドの安全を行うには、単にGILのロックを使うよりもはるかに難しいです。また、変更の対象は20年の歴史を持つCPythonコードツリーであり、さらに多くの第三者の拡張もGILに依存している。Pythonコミュニティにとって、これは刀を振るって宮から来たのと同じです。自宮でも成功したとは限らないです。ある牛の人はもう一つの検証用のCPythonを作って、GILを取り除いて、もっと多くの細かい錠を入れました。しかし、実際のテストを経て、単スレッドプログラムにとっては、このバージョンは大きな性能低下があり、利用する物理CPUが一定数を超えてからこそ、GILバージョンよりも優れている。それも無理はない。単スレッドはもともとロックは必要ないです。錠の管理そのものについて言えば、GILという太い粒度の錠は、多くの細粒度を管理する錠よりもずっと早いに違いない。今はほとんどのpythonプログラムがシングルスレッドです。なお、需要から言えば、pythonを使うのは決して演算性能が気に入っているからではない。マルチコアを利用できても、その性能はC/C++と肩を並べることができません。力を入れてGILを外したら、ほとんどのプログラムが遅くなります。これは正反対ですか?Pythonのような優秀な言語は本当に困難と意義を変えるだけで多核時代を放棄するのではないですか?実は、変化をしない一番重要な原因はまだあります。その他の神功
GILを切る以外にも、やはりPythonを多核時代に生かす方法がありますか?本論文の最初の問題に戻りましょう。どうやってこのデッドサイクルのPythonシナリオをデュアルコアマシンで100%CPUを占有することができますか?実は一番簡単な答えは2つのpythonのデッドサイクルを実行するプログラムです。つまり、CPUのカーネルを二つずつ占めるpythonプロセスで実行します。確かに、マルチプロセスも複数のCPUを利用する良い方法です。ただ、プロセス間のメモリアドレスは空間的に独立しており、互いに共同通信するのはマルチスレッドよりも面倒くさいです。感谢しています。Pythonは2.6に新しく導入されました。 multiprocessing この多プロセス標準ライブラリは、多プロセスのpythonプログラムを多スレッドのような程度に簡略化し、GILによる多核の利用ができないという気まずさを大いに軽減しています。
これはまだ一つの方法です。多プロセスというヘビー級のソリューションを使いたくないなら、もっと徹底的なプランがあります。Pythonを放棄して、C/C++に変えます。もちろん、あなたもこのようにする必要はなくて、肝心な部分をC/C++でPythonに書いて拡張するだけで、その他の部分はやはりPythonで書いて、Pythonの帰Pythonを譲って、Cの帰C。一般的に計算集約的なプログラムはCコードで作成され、Pythonスクリプトに拡張されます。拡張には、完全にCを使って原生スレッドを作成することができます。また、GILをロックすることなく、CPUの計算リソースを活用します。しかし、Pythonを書くと拡張がいつも複雑に感じられます。幸い、PythonにはCモジュールとのコミュニケーションがもう一つの機構があります。
ctypesを利用してGILをバイパスする。
ctypesはPython拡張と違って、任意のCダイナミックライブラリの導出関数をPythonに直接呼び出させることができます。あなたがするのはctypesでpythonコードを書いてもいいです。一番かっこいいのは、ctypesはC関数を呼び出す前にGILをリリースします。したがって、我々は、ctypesとCダイナミックライブラリを通じて、pythonが物理カーネルの計算能力を十分に利用できるようにすることができます。今回はCでデッドサイクル関数を書きます。
extern"C"
{
  void DeadLoop()
  {
    while (true);
  }
}
上のCコードでコンパイルしてダイナミックライブラリを生成します。 libdead_loop.so (Windowsでは dead loop.dll)
を選択します。次に、pythonではctypesを利用して、メインスレッドと新規スレッドでそれぞれ呼び出します。 DeadLoop
from ctypes import *
from threading import Thread

lib = cdll.LoadLibrary("libdead_loop.so")
t = Thread(target=lib.DeadLoop)
t.start()

lib.DeadLoop()
今回は、system monitorを見てみます。Pythonトランシーバーは2つのスレッドが走っています。しかも、2つのコアCPUは全部埋まっています。ctypesは確かに力があります。注意したいのは、GILはC関数を呼び出す前にctypesによってリリースされます。しかし、Python解釈器は、任意のPythonコードを実行する時にGILをロックします。PythonのコードをC関数のcalbackとして使うと、Pythonのcalback方法が実行された時にGILが飛び出します。例えば、次の例:
extern"C"
{
  typedef void Callback();
  void Call(Callback* callback)
  {
    callback();
  }
}
from ctypes import *
from threading import Thread

def dead_loop():
    while True:
        pass

lib = cdll.LoadLibrary("libcall.so")
Callback = CFUNCTYPE(None)
callback = Callback(dead_loop)

t = Thread(target=lib.Call, args=(callback,))
t.start()

lib.Call(callback)
ここは前の例と違っています。今回のデッドサイクルはPythonコードで発生します。 Cコードはこのcalbackを呼び出すだけです。この例を実行すると、CPUの占有率は50%以下ですか?GILがまた役に立った。
上記の例からも、ctypesの応用の一つが分かります。Pythonで自動化テスト用の例を書いて、ctypesで直接Cモジュールのインターフェースを呼び出して、このモジュールをブラックボックスでテストします。このモジュールCインターフェースのマルチスレッド安全に関するテストでも、ctypesは同じようにできます。
おわりに
CPythonのスレッドライブラリはオペレーティングシステムの元のスレッドをカプセル化しているが、GILの存在によって複数のCPUコアの計算能力が使えなくなる。幸いに今Pythonには易経筋、吸星大法(C言語拡張機構)、独孤九剣(ctypes)があります。多核時代の挑戦に十分対応しています。GILカットはまだ重要ではないですよね。