マルチスレッドおよびマルチプロセスPythonアプリケーションの作成方法の学習

この記事は、このスネークボールを解くのが子供の楽しみであるベテランのPythonテイマー向けではなく、最近Pythonに夢中になったマルチスレッド機能の表面的な概要です。



残念ながら、ロシア語ではPythonのマルチスレッドのトピックに関する資料はあまりなく、たとえばGILについて何も聞かなかったpythonersは、うらやましいほどの規則性で私に​​出会い始めました。 この記事では、マルチスレッドpythonの最も基本的な機能について説明し、GILとは何か、GILと一緒に(またはそれなしで)生きる方法などについて説明します。





Pythonは魅力的なプログラミング言語です。 多くのプログラミングパラダイムを完全に組み合わせています。 プログラマが遭遇する可能性のあるタスクのほとんどは、ここで簡単に、エレガントに、簡潔に解決されます。 しかし、これらすべてのタスクについては、シングルスレッドソリューションで十分な場合が多く、通常、シングルスレッドプログラムは予測可能でデバッグが容易です。 これは、マルチスレッドおよびマルチプロセスプログラムの場合ではありません。



マルチスレッドアプリケーション



Pythonにはスレッドモジュールがあり、マルチスレッドプログラミングに必要なすべてのものがあります。さまざまなタイプのロック、セマフォ、イベントメカニズムがあります。 つまり、大部分のマルチスレッドプログラムに必要なものすべてです。 さらに、これらのツールをすべて使用するのは非常に簡単です。 2つのスレッドを起動するプログラムの例を考えてみましょう。 1つのスレッドは10個の「0」を書き込み、他のスレッドは10個の「1」を書き込み、 厳密に順番に書き込みます。



import threading def writer(x, event_for_wait, event_for_set): for i in xrange(10): event_for_wait.wait() # wait for event event_for_wait.clear() # clean event for future print x event_for_set.set() # set event for neighbor thread # init events e1 = threading.Event() e2 = threading.Event() # init threads t1 = threading.Thread(target=writer, args=(0, e1, e2)) t2 = threading.Thread(target=writer, args=(1, e2, e1)) # start threads t1.start() t2.start() e1.set() # initiate the first event # join threads to the main thread t1.join() t2.join()
      
      





魔法やブードゥー教のコードはありません。 コードは明確で一貫しています。 さらに、ご覧のとおり、関数からストリームを作成しました。 小さなタスクには非常に便利です。 このコードは非常に柔軟です。 「2」を書き込む3番目のプロセスがあるとすると、コードは次のようになります。



 import threading def writer(x, event_for_wait, event_for_set): for i in xrange(10): event_for_wait.wait() # wait for event event_for_wait.clear() # clean event for future print x event_for_set.set() # set event for neighbor thread # init events e1 = threading.Event() e2 = threading.Event() e3 = threading.Event() # init threads t1 = threading.Thread(target=writer, args=(0, e1, e2)) t2 = threading.Thread(target=writer, args=(1, e2, e3)) t3 = threading.Thread(target=writer, args=(2, e3, e1)) # start threads t1.start() t2.start() t3.start() e1.set() # initiate the first event # join threads to the main thread t1.join() t2.join() t3.join()
      
      





新しいイベント、新しいスレッドを追加し、パラメータをわずかに変更しました

スレッドが開始します(もちろん、たとえばMapReduceを使用してより一般的なソリューションを作成できますが、これは既にこの記事の範囲外です)。

ご覧のとおり、まだ魔法はありません。 すべてがシンプルで明確です。 さらに進みましょう。



グローバルインタープリターロック



スレッドを使用する最も一般的な理由は2つあります。1つ目は、最新のプロセッサのマルチコアアーキテクチャの使用効率を向上させるためです。したがって、プログラムのパフォーマンスが向上します。

次に、プログラムのロジックを完全にまたは部分的に非同期の並列セクションに分割する必要がある場合(たとえば、同時に複数のサーバーにpingを実行できるようにする場合)。



最初のケースでは、グローバルインタープリターロック(または略してGIL)など、Python(またはCPythonの主要な実装)のような制限に直面しています。 GILの概念は、常に1つのスレッドのみがプロセッサで実行できるということです。 これは、個別の変数に対する個別のスレッド間での闘争がないように行われます。 実行可能スレッドは、環境全体にアクセスできます。 Pythonのスレッド実装のこの機能により、スレッドの処理が大幅に簡素化され、特定のスレッドの安全性が確保されます。



ただし、微妙な点があります。マルチスレッドアプリケーションは、シングルスレッドアプリケーションとまったく同じように動作し、同じことを行うか、CPU上の各スレッドの実行時間の合計で動作するように見える場合があります。 しかし、ここで私たちは1つの不快な効果を待っています。 プログラムを検討してください:



 with open('test1.txt', 'w') as fout: for i in xrange(1000000): print >> fout, 1
      
      





このプログラムは、ファイルに100万行の「1」を書き込むだけで、コンピューター上で約0.35秒で実行します。



別のプログラムを検討してください:



 from threading import Thread def writer(filename, n): with open(filename, 'w') as fout: for i in xrange(n): print >> fout, 1 t1 = Thread(target=writer, args=('test2.txt', 500000,)) t2 = Thread(target=writer, args=('test3.txt', 500000,)) t1.start() t2.start() t1.join() t2.join()
      
      





このプログラムは2つのスレッドを作成します。 各スレッドで、彼女は50万行の「1」を個別のファイルに書き込みます。 実際、作業量は以前のプログラムと同じです。 しかし、時間が経つにつれて、ここで興味深い効果が得られます。 プログラムは0.7秒から最大7秒まで実行できます。 なぜこれが起こっているのですか?



これは、スレッドがCPUリソースを必要としない場合、GILを解放し、その瞬間に自分自身と別のスレッド、およびメインスレッドの両方で試行できるためです。 さらに、多くのコアがあることを知っているオペレーティングシステムは、コア間でスレッドを分散しようとすることにより、すべてを悪化させる可能性があります。



UPD:Python 3.2では現在、GILの実装が改善されており、特に、制御が失われた後の各スレッドがGILを再びキャプチャできるようになるまで短時間待機するため、この問題は部分的に解決されています(英語での良いプレゼンテーション



「効率的なマルチスレッドプログラムをPythonで書くことはできないのですか?」とあなたは尋ねます。 いいえ、もちろん、解決策はありますが、いくつかあります。



マルチプロセスアプリケーション



ある意味で、前の段落で説明した問題を解決するために、Pythonにはサブプロセスモジュールがあります。 並列スレッド(実際には既にプロセス)で実行するプログラムを作成できます。 そして、別のプログラムの1つ以上のスレッドで実行します。 このような方法は、GIL起動プログラムで作成されたスレッドがピックアップせず、実行中のプロセスが完了するのを待つだけなので、プログラムの作業を本当にスピードアップします。 ただし、この方法には多くの問題があります。 主な問題は、プロセス間でデータを転送することが難しくなることです。 どうにかしてオブジェクトをシリアル化し、PIPEまたは他のツールを介して通信を確立する必要がありますが、これには必然的にオーバーヘッドコストがかかり、コードが理解しにくくなります。



ここでは、別のアプローチが役立ちます。 Pythonにはマルチプロセッシングモジュールがあります。 機能的には、このモジュールはスレッド化に似ています。 たとえば、通常の関数から同じ方法でプロセスを作成できます。 プロセスを操作する方法は、スレッド化モジュールのスレッドの場合とほぼ同じです。 ただし、プロセスを同期してデータを交換するには、他のツールを使用するのが一般的です。 これらはキューとパイプです。 ただし、スレッドにあったロック、イベント、およびセマフォの類似物もここにあります。



さらに、マルチプロセッシングモジュールには、共有メモリを操作するメカニズムがあります。 このため、モジュールには変数(値)および配列(配列)のクラスがあり、プロセス間で「一般化」(共有)できます。 シェア変数を操作するために、マネージャークラス(マネージャー)を使用できます。 それらはより柔軟で使いやすいですが、遅いです。 multiprocessing.sharedctypesモジュールを使用してctypesモジュールから型を共有する好機に注意することは間違いありません。



マルチプロセッシングモジュールには、プロセスプールを作成するメカニズムもあります。 このメカニズムは、Master-Workerテンプレートの実装または並列Map(ある意味ではMaster-Workerの特殊なケース)の実装に使用すると非常に便利です。



マルチプロセッシングモジュールを使用する主な問題のうち、このモジュールの相対的なプラットフォーム依存性に注目する価値があります。 プロセスの処理はオペレーティングシステムごとに異なるように編成されているため、コードにはいくつかの制限が課せられます。 たとえば、Windowsにはforkメカニズムがないため、プロセス分離ポイントを次のようにラップする必要があります。



 if __name__ =='__main__':
      
      





ただし、この設計はすでに良好な状態です。



他に何...



Pythonで並列アプリケーションを作成するための他のライブラリとアプローチがあります。 たとえば、Hadoop + PythonまたはPythonのさまざまなMPI実装(pyMPI、mpi4py)を使用できます。 既存のC ++またはFortranライブラリのラッパーを使用することもできます。 ここでは、Pyro、Twisted、Tornadoなど、多くのフレームワーク/ライブラリに言及できます。 しかし、これはすでにこの記事の範囲外です。



私のスタイルが気に入ったら、次の記事で、 PLYで簡単なインタープリターを作成する方法と、それらを使用できる理由を説明します。



All Articles