Python のマルチスレッドとマルチプロセッシングとその違い

目次

序章

1マルチプロセス

1.1フォーク法

 1.2 マルチプロセッシング方式

1.3 プール方式

1.4 プロセス間通信

1 キュー

2 パイプ

2 マルチスレッド

2.1 スレッド化

2.2 スレッドの同期

2.3 デッドロックと再帰的ロック

1 デッドロック

2 再帰的ロック


序章

初心者の場合は、まずスレッドの概念と、プログラミングにマルチスレッドを使用する必要がある理由を理解する必要があります。スレッドとは何ですか? 一般にインターネット上では、スレッドとは、オペレーティング システムが計算のスケジューリングを実行できる最小単位であり、プロセスに含まれ、プロセスにおける実際の動作単位であると定義されています。それを聞いて唖然としますか? この定義はまったくの独り言だと思いますが、初心者はこれを読んで唖然とした表情をします。したがって、平易な言葉で説明できます。

  • あなたが不動産管理会社を経営しているとします。最初は業務量が非常に少なく、すべてを自分でやらなければなりませんでした。隣の古い王さんの家の暖房用パイプを修理した後、すぐにラオ・リーさんの家に行って電球を交換しました。これをシングルといいます。 -スレッド化されており、すべての作業を順番に実行する必要がありました。
  • その後、ビジネスが拡大したとき、不動産会社が同時に複数の顧客にサービスを提供できるように、数人の従業員を雇用しました。これはマルチスレッドと呼ばれ、あなたがメインスレッドになります。
  • 従業員が使用するツールは不動産管理会社によって提供され、全員で共有されます。これはマルチスレッド リソース共有と呼ばれます。
  • 作業者は作業にパイプ レンチを必要としますが、パイプ レンチは 1 つだけです。これを競合といいます
  • 競合を解決するには、キューに入れる、同僚がなくなった後の電話通知など、さまざまな方法があります。これはスレッド同期と呼ばれます。
  • 業務が忙しくないときはオフィスでお茶を飲み、勤務時間外になるとグループでWeChatを送信し、目の前の仕事が完了したかどうかに関係なく、従業員全員がすぐに道具を置いて立ち去ります。 。したがって、必要に応じて、作業者が忙しいときに非番通知を送信することを避ける必要があります。これをスレッド ガード属性の設定と管理と呼びます。
  • その後、貴社は規模を拡大し、同時に多くの生活圏にサービスを提供するようになりましたが、生活圏ごとに支店を設置し、支店長が支店を管理するという運営体制は、社長とほぼ同じでした。オフィス - これはマルチプロセスと呼ばれ、本社はメインプロセス、ブランチはサブプロセスと呼ばれます。
  • 本社と各支店間ではツールが独立しており、ツールの貸し借りや混在ができません。これを工程間でリソースを共有できないといいます支店は専用の電話回線を介して接続できます。これはパイプラインと呼ばれます。各支店は社内掲示板を通じて情報を交換することもできます。これは共有メモリと呼ばれます。
  • 支社は本社と一緒に退勤することも、その日のすべての作業を終えてから退勤することもできます。これはデーモン設定と呼ばれます。

1マルチプロセス

Python でマルチプロセッシングを実装するには、主に 2 つの方法があります。os モジュールでの fork メソッドと multiprocessing モジュールです。前者は Unix/Linux システムにのみ適用できますが、後者はクロスプラットフォーム実装です。

1.1フォーク法

import os

# 注意,fork函数,只在Unix/Linux/Mac上运行,windows不可以
pid = os.fork()

if pid == 0:
    print('哈哈1')
else:
    print('哈哈2')

注: fork() 関数は Unix/Linux/Mac でのみ実行でき、Windows では実行できません。

例証します:

  • プログラムが os.fork() を実行すると、オペレーティング システムは新しいプロセス (子プロセス) を作成し、親プロセスのすべての情報を子プロセスにコピーします。
  • その後、親プロセスと子プロセスの両方が fork() 関数からの戻り値を取得します。この値は、子プロセスでは 0、親プロセスでは子プロセスの ID 番号でなければなりません。

Unix/Linux オペレーティング システムでは、非常に特殊な fork() システム関数が提供されています。通常の関数呼び出しは 1 回呼び出して 1 回戻りますが、fork() は 1 回呼び出して 2 回戻ります。これは、オペレーティング システムが現在のプロセス (親プロセスと呼ばれる) (子プロセスと呼ばれる) を自動的にコピーし、それぞれが返されるためです。親プロセスと子プロセス内。子プロセスは常に 0 を返しますが、親プロセスは子プロセスの ID を返します。

その理由は、親プロセスが多くの子プロセスをフォークできるため、親プロセスは各子プロセスの ID を記録する必要があり、子プロセスは親プロセスの ID を取得するために getppid() を呼び出すだけでよいためです。現在のプロセス ID は os.getpid() を通じて取得でき、親プロセス ID は os.getppid() を通じて取得できます。

では、親プロセスと子プロセスの間に実行順序はあるのでしょうか? 答えはいいえだ!それはすべて、オペレーティング システムのスケジューリング アルゴリズムに依存します。

複数の fork() はツリー構造を生成します。

 1.2 マルチプロセッシング方式

multiprocessing は、プロセス オブジェクトを記述する Process クラスを提供します。子プロセスを作成するときは、実行関数と関数パラメータを渡すだけで済みます。start () メソッドを使用してプロセスを開始し、join () メソッドを使用してプロセス間の同期を実現します。

import os
from multiprocessing import Process

def run_proc(name):
    print("Child process (%s) (%s) running..." % (name, os.getpid()))
    

if __name__ == '__main__':
    print("Current process (%s) start..." % (os.getpid()))
    for i in range(5):
        p = Process(target=run_proc, args=str(i))
        print("Process will start.")
        p.start()
    p.join()
    print("Process end.")

出力は次のとおりです。

Current process (26811) start...
Process will start.
Process will start.
Process will start.
Child process (0) (26872) running...
Process will start.
Child process (1) (26874) running...
Process will start.
Child process (2) (26876) running...
Child process (3) (26882) running...
Child process (4) (26885) running...
Process end.

1.3 プール方式

Process メソッドを使用する場合、多数の子プロセスを起動する必要があるという欠点がありますが、これは操作対象のオブジェクトの数が多くない場合にのみ当てはまりますが、Pool を使用することでこの問題を解決できます。簡単に言えば、Pool はプロセスの数を指定できます。デフォルトは CPU コアの数で、最大で指定された数のプロセスを同時に実行できます。

import os, time, random
from multiprocessing import Pool

def run_task(name):
    print("Task %s (pid = %s) is running..." % (name, os.getpid()))
    time.sleep(random.random() * 3)
    print("Task %s end." % name)

if __name__ == '__main__':
    print("Current process (%s) start..." % (os.getpid()))
    p = Pool(processes=3)
    for i in range(5):
        p.apply_async(run_task, args=(i, ))
    print("Waiting for all subprocess done...")
    p.close()
    p.join()
    print("All subprocess done.")

出力は次のとおりです。

Current process (4202) start...
Waiting for all subprocess done...
Task 0 (pid = 4255) is running...
Task 1 (pid = 4256) is running...
Task 2 (pid = 4257) is running...
Task 0 end.
Task 3 (pid = 4255) is running...
Task 2 end.
Task 4 (pid = 4257) is running...
Task 4 end.
Task 1 end.
Task 3 end.
All subprocess done.

Pool オブジェクトの join() メソッドを呼び出すと、すべての子プロセスの実行が完了するまで待機するため、join() を呼び出す前に close() を呼び出す必要があることに注意してください。close() を呼び出した後は、新しいプロセスを追加し続けることはできません。プロセス。

1.4 プロセス間通信

Python にはさまざまなプロセス間通信手段が用意されていますが、この記事では主に Queue と Pipe について説明します。

1 キュー

主に複数プロセス間の通信に使用され、動作は以下の通りです。

  • Queue クラスをインスタンス化する場合、q = Queue(5) のように最大数のメッセージを渡すことができます。このコードは、メッセージ キュー内で最大 5 つのメッセージ データのみが許可されることを意味します。メッセージの最大数が追加されていない場合、または数値が負の場合、メモリがいっぱいになるまで式は数を制限しません。 
  • Queue.qsize(): 現在のキューに含まれるメッセージの数を返します。 
  • Queue.empty(): キューが空の場合は True を返し、それ以外の場合は False を返します。 
  • Queue.full(): キューがいっぱいの場合は True を返し、それ以外の場合は False を返します。 
  • Queue.get([block[, timeout]]): キュー内のメッセージを取得し、キューから削除します。block のデフォルト値は True です。 
    • ブロックがデフォルト値を使用し、タイムアウト (秒単位) を設定しない場合、メッセージ キューが空の場合、メッセージがメッセージ キューから読み取られるまでプログラムはブロックされます (読み取り状態で停止します)。 、その後、タイムアウト秒間待機し、メッセージが読み取られていない場合は、「Queue.Empty」例外がスローされます。 
    • ブロック値が False の場合、メッセージ キューが空の場合は、すぐに「Queue.Empty」例外がスローされます。 
  •  Queue.get_nowait():相当Queue.get(False); 
  • Queue.put(item,[block[, timeout]]): 項目メッセージをキューに書き込みます。block のデフォルト値は True です。
    • ブロックがデフォルト値を使用し、タイムアウト (秒単位) が設定されていない場合、メッセージ キューに書き込むスペースがない場合、プログラムはメッセージ キューに空きができるまでブロックされます (書き込み状態で停止します)。設定されている場合 タイムアウトが設定されている場合は、タイムアウト秒間待機し、余裕がない場合は、「Queue.Full」例外がスローされます。 
    • ブロック値が False の場合、メッセージ キューに書き込むスペースがない場合は、すぐに「Queue.Full」例外がスローされます。 
  • Queue.put_nowait(item):相当Queue.put(item, False);

  通信にキューを使用する方法をわかりやすく説明するために、次の例を示します。親プロセス内に 3 つの子プロセスを作成し、そのうちの 2 つはキューにデータを書き込み、もう 1 つの子プロセスはキューからデータを読み取ります。

import os, time
from multiprocessing import Process, Queue

"""Write to process."""
def proc_write(q, urls):
    print("Process (%d) is writing..." % os.getpid())
    for url in urls:
        q.put(url)
        print('Put %s to queue...' % url)
        time.sleep(0.1)

"""Read form process."""    
def proc_read(q):
    print("Process (%d) is reading..." % (os.getpid()))
    while True:
        url = q.get(True)
        print("Get %s from queue." % url)

if __name__ == '__main__':
    # 创建父进程
    q = Queue()
    writer1 = Process(target=proc_write, args=(q, ['张飞', '黄忠', "孙尚香"]))
    writer2 = Process(target=proc_write, args=(q, ['马超', '关羽', "赵云"]))
    reader = Process(target=proc_read, args=(q, ))
    # 启动
    writer1.start()
    writer2.start()
    reader.start()
    writer1.join()
    writer2.join()
    # 读操作是死循环,必须强行终止
    reader.terminate()

出力は次のとおりです。

Process (19307) is writing...
Put 张飞 to queue...
Process (19308) is writing...
Put 马超 to queue...
Process (19309) is reading...
Get 张飞 from queue.
Get 马超 from queue.
Put 黄忠 to queue...
Get 黄忠 from queue.
Put 关羽 to queue...
Get 关羽 from queue.
Put 孙尚香 to queue...
Get 孙尚香 from queue.
Put 赵云 to queue...
Get 赵云 from queue

2 パイプ

Pipe()は、 Pipeの両端を表す 2 つの接続オブジェクトを返します。各接続オブジェクトにはsend()メソッドとrecv()メソッドがあります。

ただし、2 つのプロセスまたはスレッド オブジェクトがパイプの両端で同時にデータの読み取りまたは書き込みを行うと、パイプ内のデータが破損する可能性があります。プロセスがパイプの両端で異なるデータを使用している場合、データ破損のリスクはありません。

import os, time
from multiprocessing import Process, Pipe

def proc_send(p, urls):
    print("Process (%d) is sending..." % os.getpid())
    for url in urls:
        p.send(url)
        print('Send %s...' % url)
        time.sleep(0.1)

def proc_recv(p):
    print("Process (%d) is receiving..." % (os.getpid()))
    while True:
        print("Receive %s" % p.recv())
        time.sleep(0.1)

if __name__ == '__main__':
    # 创建父进程
    p = Pipe()
    p1 = Process(target=proc_send, args=(p[0], ['张飞' + str(i) for i in range(3)]))
    p2 = Process(target=proc_recv, args=(p[1], ))
    p1.start()
    p2.start()
    p1.join()
    p2.join()

出力は次のとおりです。

Process (39203) is sending...
Send 张飞0...
Process (39204) is receiving...
Receive 张飞0
Send 张飞1...
Receive 张飞1
Send 张飞2...
Receive 张飞2

2 マルチスレッド

マルチスレッドは複数の異なるプログラムの実行に似ており、次の利点があります。

  • タスクはバックグラウンドで処理できます
  • プログレスバーなど、ユーザーインターフェイスはもっと魅力的になる可能性があります
  • プログラムの速度が向上する可能性があります
  • ユーザー入力、ファイルの読み書き、ネットワークでのデータの送受信など、待機する必要がある一部のタスクでは、メモリ使用量などの一部のリソースが解放されることがあります。

Python の標準ライブラリには、thread と threading の 2 つのモジュールが用意されており、thread は低レベルのモジュール、threading はスレッドをカプセル化する高レベルのモジュールです。ほとんどの場合、高度なモジュール スレッドを使用するだけで済みます。スレッド化のその他のメソッド.Thread:

  • start: スレッドは準備ができており、CPU スケジューリングを待っています。
  • setName: スレッドの名前を設定します。
  • getName: スレッド名を取得します
  • setDaemon: バックグラウンド スレッドまたはフォアグラウンド スレッド (デフォルト) に設定します。バックグラウンド スレッドの場合、メイン スレッドの実行中にバックグラウンド スレッドも実行され、メイン スレッドの実行後、バックグラウンド スレッドが停止しているかどうかに関係なく停止します。成功したかどうか; フォアグラウンド スレッドの場合、メイン スレッドの実行中、フォアグラウンド スレッドも進行中です。メイン スレッドの実行後、フォアグラウンド スレッドの実行が完了するまで待機し、プログラムが停止します。
  • join: 各スレッドを1つずつ実行し、実行後も継続して実行するマルチスレッドの意味をなくす方法
  • run: スレッドが CPU によってスケジュールされた後、スレッド オブジェクトの run メソッドが自動的に実行されます。
  • ロック: スレッドロック (ミューテックス)
  • イベント

2.1 スレッド化

方法 1: 関数を渡して Thread インスタンスを作成し、start() を実行します。

import time, threading

def thread_run(urls):
    print("Current %s is running..." % threading.current_thread().name)
    for url in urls:
        print("%s --->>> %s" % (threading.current_thread().name, url))
        time.sleep(0.1)
    print("%s ended." % threading.current_thread().name)

if __name__ == '__main__':
    print("%s is running..." % threading.current_thread().name)
    t1 = threading.Thread(target=thread_run, name='t1', args=(['唐僧', '孙悟空', '猪八戒'],))
    t2 = threading.Thread(target=thread_run, name='t2', args=(['张飞', '关于', '刘备'],))
    t1.start()
    t2.start()
    t1.join()
    t1.join()
    print("%s ended." % threading.current_thread().name)

出力は次のとおりです。 

MainThread is running...
Current t1 is running...Current t2 is running...

t2 --->>> 张飞
t1 --->>> 唐僧
t2 --->>> 关于t1 --->>> 孙悟空

t2 --->>> 刘备t1 --->>> 猪八戒

t1 ended.t2 ended.

MainThread ended.

方法 2: threading.Threadから継承し てスレッド クラスを作成し、__init__メソッドとrunメソッドをオーバーライドします。

import time, threading

class testThread(threading.Thread):

    def __init__(self, name, urls):
        threading.Thread.__init__(self, name=name)
        self.urls = urls

    def run(self):
        print("Current %s is running..." % threading.current_thread().name)
        for url in self.urls:
            print("%s --->>> %s" % (threading.current_thread().name, url))
            time.sleep(0.1)
        print("%s ended." % threading.current_thread().name)

if __name__ == '__main__':
    print("%s is running..." % threading.current_thread().name)
    t1 = testThread(name='t1', urls=['唐僧', '孙悟空', '猪八戒'])
    t2 = testThread(name='t2', urls=['张飞', '关于', '刘备'])
    t1.start()
    t2.start()
    t1.join()
    t1.join()
    print("%s ended." % threading.current_thread().name)

出力は次のとおりです。

MainThread is running...
Current t1 is running...Current t2 is running...
t1 --->>> 唐僧

t2 --->>> 张飞
t1 --->>> 孙悟空t2 --->>> 关于

t1 --->>> 猪八戒t2 --->>> 刘备

t1 ended.t2 ended.

MainThread ended.

2.2 スレッドの同期

複数のスレッドが共同して特定のデータを変更すると、予期しない結果が発生する可能性があります。データの正確性を保証するには、複数のスレッドを同期する必要があります。具体的な指示:

  • 単純なスレッド同期は、Thread オブジェクトの Lock および RLock を使用することで実現できます。どちらにも、acquire() メソッドと release() メソッドがあります。
  • 一度に 1 つのスレッドのみが操作できるデータの場合、その操作をacquire() と release() の間に配置できます。
  • Lock オブジェクトの場合、スレッドがacquire()操作を2回続けて実行すると、スレッドはデッドロックになります。
  • RLock オブジェクトは、カウンタ変数を通じて内部的にacquire()の回数を維持するため、スレッドがacquire()操作を連続して複数回実行できるようにします。
  • 各acquire()オブジェクトには、それに対応するrelease()が必要です。
  • すべての release() 操作が完了すると、他のスレッドが RLock オブジェクトを申請できるようになります。
import threading

test_lock = threading.RLock()
num = 0

class testThread(threading.Thread):
    def __init__(self, name):
        threading.Thread.__init__(self, name=name)

    def run(self):
        global num
        while True:
            test_lock.acquire()
            print("%s locked, Number: %d" % (threading.current_thread().name, num))
            if num >= 4:
                test_lock.release()
                print("%s released, Number: %d" % (threading.current_thread().name, num))
                break
            num += 1
            print("%s released, Number: %d" % (threading.current_thread().name, num))
            test_lock.release()

if __name__ == '__main__':
    t1 = testThread('孙悟空先上')
    t2 = testThread("终于到八戒了")
    t1.start()
    t2.start()

出力は次のとおりです。

孙悟空先上 locked, Number: 0
孙悟空先上 released, Number: 1
孙悟空先上 locked, Number: 1
孙悟空先上 released, Number: 2
孙悟空先上 locked, Number: 2
孙悟空先上 released, Number: 3
孙悟空先上 locked, Number: 3
孙悟空先上 released, Number: 4
孙悟空先上 locked, Number: 4
孙悟空先上 released, Number: 4终于到八戒了 locked, Number: 4

终于到八戒了 released, Number: 4

2.3 デッドロックと再帰的ロック

1 デッドロック

いわゆるデッドロック:複数のプロセスやスレッドが実行処理中にリソースの奪い合いにより待ち状態となり、外部からの力がなければ先に進めなくなる現象を指します。このとき、システムがデッドロック状態にある、またはシステム内でデッドロックが発生しているといい、このように常に待ち続けているプロセスをデッドロックプロセスと呼びます。以下に示す例はデッドロックです。

from threading import Thread,Lock
import time

mutexA = Lock()
mutexB = Lock()

class testThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print('%s 拿到A锁' %self.name)

        mutexB.acquire()
        print('%s 拿到B锁' %self.name)
        mutexB.release()
        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('%s 拿到B锁' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('%s 拿到A锁' %self.name)
        mutexA.release()

        mutexB.release()

if __name__ == '__main__':
    for i in range(5):
        t = testThread()
        t.start()

出力は次のとおりです。

Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 拿到B锁Thread-2 拿到A锁

上記のコードがどのようにデッドロックを生成するかを分析します: 
5 つのスレッドを開始し、run メソッドを実行します。スレッド 1 が最初に A ロックを取得した場合、スレッド 1 はこの時点では A ロックを解放せず、コード mutexB.acquire() を実行して、 B ロック。B ロックを取得すると、A ロックが解放されず、他のスレッドは待機することしかできないため、他のスレッドはスレッド 1 と競合しません。その後、A ロックは func1 コードを実行し、その後 func2 コードの実行を続けます。同時に、func2 でコード mutexB.acquire() を実行し、B ロックを取得し、スリープ状態に入ります。スレッド 1 が func1 関数を実行して AB ロックを解放した後、残りの他のスレッドも A ロックの取得を開始します。 func1 コードを実行します。スレッド 2 が A ロックを取得した場合、次のスレッド 2 は B ロックを取得したいと考えています。わかりました。この期間中、スレッド 1 は func2 を実行して B ロックを取得し、その後 sleep(2) で B ロックを取得します。が解放されていません。解放されない理由は、それと競合するスレッドが他にないからです。取得するために、彼は眠りにつくことしかできません。その後、スレッド 1 が B ロックを保持し、スレッド 2 が B ロックを取得しようとします、わかりました、このように形成されます行き詰まり。

2 再帰的ロック

上記でデッドロックを分析しましたが、Python でデッドロックの問題を解決するにはどうすればよいでしょうか? Python の同じスレッド内の同じリソースに対する複数のリクエストをサポートするために、Python は再入可能ロック RLock を提供します。この RLock は内部でロックとカウンター変数を維持し、カウンターは取得回数を記録するため、リソースが複数回必要になる可能性があります。スレッドのすべての取得が解放されるまで、他のスレッドはリソースを取得できます。上記の例では、Lock の代わりに RLock が使用されている場合、デッドロックは発生しません。

from threading import Thread,RLock
import time

mutexA = mutexB = RLock()

class testThread(Thread):
    def run(self):
        self.f1()
        self.f2()

    def f1(self):
        mutexA.acquire()
        print('%s 拿到A锁' %self.name)
        mutexB.acquire()
        print('%s 拿到B锁' %self.name)
        mutexB.release()
        mutexA.release()

    def f2(self):
        mutexB.acquire()
        print('%s 拿到B锁' % self.name)
        time.sleep(0.1)
        mutexA.acquire()
        print('%s 拿到A锁' % self.name)
        mutexA.release()
        mutexB.release()

if __name__ == '__main__':
    for i in range(5):
        t=testThread()
        t.start()

出力は次のとおりです。

Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 拿到B锁
Thread-1 拿到A锁
Thread-2 拿到A锁
Thread-2 拿到B锁
Thread-2 拿到B锁
Thread-2 拿到A锁
Thread-4 拿到A锁
Thread-4 拿到B锁
Thread-4 拿到B锁
Thread-4 拿到A锁
Thread-3 拿到A锁
Thread-3 拿到B锁
Thread-3 拿到B锁
Thread-3 拿到A锁
Thread-5 拿到A锁
Thread-5 拿到B锁
Thread-5 拿到B锁
Thread-5 拿到A锁

再帰的ロックのコードを説明します。 
ロック A と B は同じ再帰的ロックであるため、スレッド 1 はロック A とロック B を取得し、カウンタは取得回数を 2 回記録し、func1 が実行された後に再帰的ロックを解放します。 thread1 再帰ロックの後、func1 コードを実行した後、次に 2 つの可能性があります。1、thread1 が次回再帰ロックを取得し、func2 コードを実行します。2 他のスレッドが再帰ロックを取得し、func1 のタスク コードを実行します。

おすすめ

転載: blog.csdn.net/qq_40716944/article/details/121510749