マルチタスクは、複数のプロセスによって、またはプロセス内の複数のスレッドによって完了することができます。
プロセスは複数のスレッドで構成されており、プロセスには少なくとも1つのスレッドがあることを前述しました。
スレッドはオペレーティングシステムによって直接サポートされる実行ユニットであるため、高水準言語には通常、組み込みのマルチスレッドサポートがあり、Pythonも例外ではありません。さらに、Pythonスレッドは実際のPosixスレッドであり、シミュレートされたスレッドではありません。
Pythonの標準ライブラリには、_threadとthreadingの2つのモジュールがあります。_threadは低レベルのモジュールであり、threadingは_threadをカプセル化する高レベルのモジュールです。ほとんどの場合、スレッドの高度なモジュールを使用するだけで済みます。
スレッドを開始するには、関数を渡してThreadインスタンスを作成し、start()を呼び出して実行を開始します。
import time, threading
# 新线程执行的代码:
def loop():
print('thread %s is running...' % threading.current_thread().name)
n = 0
while n < 5:
n = n + 1
print('thread %s >>> %s' % (threading.current_thread().name, n))
time.sleep(1)
print('thread %s ended.' % threading.current_thread().name)
print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)
結果は次のとおりです。
thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.
どのプロセスもデフォルトでスレッドを開始するため、このスレッドをメインスレッドと呼び、メインスレッドは新しいスレッドを開始できます。Pythonのスレッドモジュールにはcurrent_thread()関数があり、常に現在のスレッドのインスタンスを返します。メインスレッドインスタンスの名前はMainThreadであり、子スレッドの名前は作成時に指定され、 LoopThreadを使用して子スレッドに名前を付けます。この名前は、印刷時の表示にのみ使用され、他の意味はありません。名前が使用できない場合、Pythonは自動的にスレッドにThread-1、Thread-2 ..という名前を付けます。
ロック
マルチスレッドとマルチプロセスの最大の違いは、マルチプロセスでは、同じ変数が各プロセスにコピーを持ち、相互に影響を与えないことです。マルチスレッドでは、すべての変数がすべてのスレッドで共有されるため、いずれか変数はどのスレッドでも変更できるため、スレッド間でデータを共有する最大の危険性は、複数のスレッドが同時に変数を変更し、内容が変更されることです。(だからロックが必要です)
複数のスレッドの同僚が変数を操作してコンテンツを変更する方法を見てみましょう。
import time, threading
# 假定这是你的银行存款:
balance = 0
def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(2000000):
change_it(n)
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
共有変数バランスを定義し、初期値は0で、2つのスレッドを開始し、最初に保存してからフェッチします。理論的には結果は0になりますが、スレッドのスケジューリングはオペレーティングシステムによって決定されるため、t1、t2が交互に実行されます。時間、サイクル数が十分である限り、バランスの結果は必ずしも0ではありません。
その理由は、高水準言語のステートメントは、単純な計算であっても、CPUの実行時に複数のステートメントであるためです。
balance = balance + n
また、2つのステップがあります。
- 残高+ nを計算し、一時変数に格納します。
- 一時変数の値をバランスに割り当てます。
それは次のように見ることができます:
x = balance + n
balance = x
xはローカル変数であるため、2つのスレッドにはそれぞれ独自のxがあります。コードが正常に実行されると:(これが当てはまることがわかります。高級言語はステートメントをいくつかの部分に分割して実行します)
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2 # balance = 0
结果 balance = 0
ただし、オペレーティングシステムがt1とt2を次の順序で実行する場合、t1とt2は交互に実行されます。
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance - 8 # x2 = 0 - 8 = -8
t2: balance = x2 # balance = -8
结果 balance = -8
その理由は、バランスを変更するには複数のステートメントが必要であり、これらのステートメントが実行されると、スレッドが中断され、複数のスレッドが同じオブジェクトのコンテンツを台無しにする可能性があるためです。
2つのスレッドが同時に入出金する場合、残高が間違っている可能性があります。銀行預金がマイナスになることは絶対に避けてください。したがって、一方のスレッドが残高を変更するときに、もう一方のスレッドが変更されないようにする必要があります。それ。(ロック)
バランス計算が正しいことを確認したい場合は、change_it()にロックを与える必要があります。スレッドがchange_it()の実行を開始すると、スレッドがロックを取得したため、他のスレッドはでchange_it()を実行できないと言います。同時に、ロックが解除されるまで待つことができ、ロックを取得した後で変更することができます。ロックは1つしかないため、スレッドの数に関係なく、同時にロックを保持できるのは最大で1つのスレッドのみであり、変更の競合は発生しません。ロックの作成はthreading.Lock()によって実現されます:(ロックはプログラマーによって合意され、アトミック操作はロックを無視できます。Pythonのアトミック操作とは何ですか?)
balance = 0
lock = threading.Lock()
def run_thread(n):
for i in range(100000):
# 先要获取锁:
lock.acquire()
try:
# 放心地改吧:
change_it(n)
finally:
# 改完了一定要释放锁:
lock.release()
複数のスレッドが同時にlock.acquire()を実行する場合、1つのスレッドのみがロックを正常に取得してコードを実行し続け、他のスレッドはロックが取得されるまで待機し続けます。
**ロックを取得したスレッドが使い果たされた後、ロックを解除する必要があります。そうしないと、ロックを待機しているスレッドが永久に待機し、デッドスレッドになります。**したがって、try ... finalを使用して、ロックが解放されることを確認します。(デッドロックになるはずです)
ロックの利点は、特定のキーコードを最初から最後まで1つのスレッドでのみ完全に実行できるようにすることです。もちろん、多くの欠点があります。まず、複数のスレッドの同時実行を防ぎます。コードの特定のセクションロックを含むには、シングルスレッドモードでのみ実行できます。実装すると、効率が大幅に低下します。次に、複数のロックが存在する可能性があるため、異なるスレッドが異なるロックを保持し、相手が保持しているロックを取得しようとします。これにより、デッドロックが発生し、複数のスレッドがハングアップして、実行も終了もできなくなります。強制終了のみ。
マルチコアCPU
残念ながらマルチコアCPUを使用している場合は、マルチコアで複数のスレッドを同時に実行できるようにする必要があると考えている必要があります。
無限ループを書くとどうなりますか?
Mac OS Xのアクティビティモニター、またはWindowsのタスクマネージャーを開くと、特定のプロセスのCPU使用率を監視できます。
無限ループスレッドがCPUを100%占有することを監視できます。
マルチコアCPUにエンドレスループスレッドが2つある場合、CPUの200%を占める、つまり2つのCPUコアを占めることを監視できます。
NコアCPUのすべてのコアを実行する場合は、N個のエンドレスループスレッドを開始する必要があります。
Pythonで無限ループを書いてみてください:(私は試しません)
import threading, multiprocessing
def loop():
x = 0
while True:
x = x ^ 1
for i in range(multiprocessing.cpu_count()):
t = threading.Thread(target=loop)
t.start()
同じ数のCPUコアでNスレッドを開始します.4コアCPUでは、CPU占有率がわずか102%、つまり1つのコアのみが使用されていることを監視できます。
しかし、C、C ++、またはJavaを使用して同じ無限ループを書き換えると、すべてのコアを直接実行でき、4コアは400%まで実行され、8コアは800%まで実行されます。Pythonが機能しないのはなぜですか。
** Pythonスレッドは実際のスレッドですが、インタープリターがコードを実行すると、GILロック(グローバルインタープリターロック)が発生します。Pythonスレッドを実行する前に、まずGILロックを取得する必要があります。その後、100バイトのコードごとにが実行されると、インタプリタは自動的にGILロックを解放し、他のスレッドが実行できるようにします。**このGILグローバルロックは実際にはすべてのスレッドの実行コードをロックします。したがって、マルチスレッドはPythonで交互にしか実行できません。100コアのCPUで100スレッドを実行しても、使用できるのは1つだけです。
GILはPythonインタープリターの設計の遺産です。通常、使用するインタープリターはCPythonの公式実装であり、GILなしでインタープリターを書き直さない限り、本当にマルチコアを使用する必要があります。
したがって、Pythonではマルチスレッドを使用できますが、マルチコアを効果的に使用することは期待できません。複数のスレッドを介して複数のコアを使用する必要がある場合は、C拡張機能を介してのみ実現できますが、これによりPythonのシンプルさと使いやすさが失われます。
ただし、あまり心配する必要はありません。Pythonはマルチスレッドを使用してマルチコアタスクを実行することはできませんが、複数のプロセスを通じてマルチコアタスクを実行することはできます。複数のPythonプロセスには、相互に影響を与えない独自の独立したGILロックがあります。
概要
マルチスレッドプログラミングには複雑なモデルがあり、競合が発生しやすいため、ロックを使用してそれらを分離する必要があります。同時に、デッドロックに注意する必要があります。
PythonインタープリターはGILグローバルロックを使用して設計されているため、複数のスレッドが複数のコアを使用することはできません。マルチスレッドの同時実行性は、Pythonの美しい夢です。