Pythonプロセスとスレッドのマルチスレッド

Pythonの研究ノート、特別な記録、あなたと共有してください、私はそれが皆に役立つことを願っています。

マルチスレッド

マルチタスクは、複数のプロセスによって、またはプロセス内の複数のスレッドによって完了することができます。

プロセスは複数のスレッドで構成されており、プロセスには少なくとも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.

Process finished with exit code 0

どのプロセスもデフォルトでスレッドを開始するため、このスレッドをメインスレッドと呼び、メインスレッドは新しいスレッドを開始できます。Pythonのスレッドモジュールにはcurrent_thread()関数があり、常に現在のスレッドのインスタンスを返します。メインスレッドインスタンスの名前はMainThreadであり、子スレッドの名前は作成時に指定され、LoopThreadを使用して子スレッドに名前を付けます。この名前は印刷時の表示にのみ使用され、他の意味はまったくありません。名前を付ける余裕がない場合、Pythonは自動的にスレッドにThread-1、Thread-2 ...という名前を付けます。

ロック

マルチスレッドとマルチプロセスの最大の違いは、マルチプロセスでは、同じ変数が各プロセスにコピーを持ち、相互に影響を与えないことです。マルチスレッドでは、すべての変数がすべてのスレッドで共有されるため、いずれか1つです。変数はどのスレッドでも変更できるため、スレッド間でデータを共有する最大の危険性は、複数のスレッドが同時に変数を変更し、内容が変更されることです。

複数のスレッドが同時に変数を操作してコンテンツを変更する方法を見てみましょう。

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(100000):
        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つのステップに分かれています。

  1. バランス+ nを計算し、一時変数に格納します。
  2. 一時変数の値をバランスに割り当てます。

それは次のように見ることができます:

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()によって実現されます。

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バイトコードごとにインタープリターItが実行されます。 GILロックを自動的に解放し、他のスレッドが実行できるようにします。このGILグローバルロックは、実際にはすべてのスレッドの実行コードをロックするため、Pythonでは複数のスレッドを交互に実行することしかできません。100コアのCPUで100スレッドを実行しても、使用できるのは1コアのみです。

GILはPythonインタープリターの設計の歴史的な問題です。通常、使用するインタープリターはCPythonの公式実装であり、GILなしでインタープリターを書き直さない限り、本当にマルチコアを使用する必要があります。

したがって、Pythonでは複数のスレッドを使用できますが、複数のコアを効果的に使用することは期待できません。マルチスレッドを介して複数のコアを使用する必要がある場合、それはC拡張を介してのみ実現できますが、これはPythonの単純さと使いやすさを失います。

ただし、あまり心配する必要はありません。Pythonはマルチスレッドを使用してマルチコアタスクを実行することはできませんが、複数のプロセスを通じてマルチコアタスクを実行することはできます。複数のPythonプロセスには、相互に影響を与えない独自の独立したGILロックがあります。

おすすめ

転載: blog.csdn.net/qq_36478920/article/details/99828910