1 か月 (30) で Python を学ぶ: Python 同時プログラミング (オン)

コラム紹介

私自身の経験と社内資料を組み合わせてPythonチュートリアルをまとめており、1日3~5章、最短1か月でPythonの学習を総合的に完了し、実践的な開発を行うことができます。必ずボスになります!来て!巻き上げる!

すべての記事については、コラム「Python フルスタック チュートリアル (基礎 0)」
を参照してください。また、最新の更新をお勧めします。「大昌試験で高頻度の面接質問の詳細な説明」このコラムでは、高頻度の面接質問に対する詳細な回答を提供します。 - 近年の頻度テストを、あなた自身の長年の実務経験、および同僚のリーダーの指導と組み合わせて実施しました。テストや Python を勉強している学生が面接をスムーズに通過し、満足のいく内定を獲得できるように支援することを目的としています。



Python での同時プログラミング

現在、私たちが使用するコンピューターはすでにマルチ CPU またはマルチコア コンピューターになっており、私たちが使用するオペレーティング システムは基本的に「マルチタスク」をサポートしています。これにより、複数のプログラムを同時に実行したり、プログラムを複数の相対的なタスクに分解したりすることができます。独立したサブタスクにより、複数のサブタスクを「並列」または「同時」に実行できるため、プログラムの実行時間が短縮され、ユーザーはより良いエクスペリエンスを得ることができます。そのため現在では、どのようなプログラミング言語を使って開発しても、「並列」または「同時」プログラミングを実現することがプログラマの標準的なスキルとなっています。Python プログラムで「並列」または「同時実行」を実現する方法を説明するには、プロセスとスレッドという 2 つの重要な概念を理解する必要があります。

スレッドとプロセス

オペレーティング システムを通じてプログラムを実行すると、1 つまたは複数のプロセスが作成されます。プロセスとは、特定のデータ セットでの実行アクティビティに関する特定の独立した機能を持つプログラムです。簡単に言うと、プロセスは、オペレーティング システムがストレージ スペースを割り当てるための基本単位です。各プロセスには、プロセスの実行を追跡するために使用される独自のアドレス スペース、データ スタック、およびその他の補助データがあります。オペレーティング システムは、すべてのプロセスの実行を管理します。を処理し、適切な割り当てリソースを提供します。プロセスは他のタスクを実行するためにフォークまたはスポーンによって新しいプロセスを作成できますが、新しいプロセスには独自の独立したメモリ空間もあるため、2 つのプロセスがデータを共有したい場合は、プロセス間通信メカニズムを通じて実現する必要があります。具体的には、メソッドにはパイプ、シグナル、ソケットなどが含まれます。

プロセスには複数の実行スレッドを持つこともできます。簡単に言えば、CPU によってスケジュールできる複数の実行ユニットがあります。これがいわゆるスレッドです。スレッドは同じプロセスの下にあるため、同じコンテキストを共有できるため、プロセス間よりもスレッド間の情報共有と通信が容易になります。もちろん、シングルコア CPU システムでは、複数のスレッドを同時に実行することはできません。ある瞬間に CPU を取得できるのは 1 つのスレッドだけであり、複数のスレッドが CPU の実行時間を共有することで同時実行性を実現するからです。

プログラムでマルチスレッド テクノロジを使用すると、通常、プログラムのパフォーマンスの向上とユーザー エクスペリエンスの向上に特に顕著なメリットがもたらされます。現在私たちが使用しているほとんどすべてのソフトウェアはマルチスレッド テクノロジを使用しており、これをシステムが利用できる可能性があります。組み込みのプロセス監視ツール (macOS の「アクティビティ モニター」、Windows の「タスク マネージャー」など) によって確認されます。

ここで、並行性並列性という 2 つの概念を再度強調する必要があります同時実行性は通常、一度に 1 つの命令しか実行できないことを意味しますが、複数のスレッドに対応する命令は高速回転で実行されます。たとえば、プロセッサは最初にスレッド A の命令を一定期間実行し、次にスレッド B の命令を一定期間実行し、その後スレッド A に戻って一定期間実行します。プロセッサの実行速度と切り替え速度が非常に速いため、コンピュータの処理中に複数のスレッドがコンテキストを切り替えて実行する操作に人々はまったく気付かず、マクロからは複数のスレッドが同時に実行されているように見えます。実際には 1 つのスレッドだけが実行されています。並列処理とは、複数のプロセッサ上で複数の命令を同時に実行することを指します。並列処理は複数のプロセッサに依存する必要があります。マクロであってもマイクロであっても、複数のスレッドを同時に実行できます。多くの場合、同時実行と並列処理は厳密に区別されていないため、Python ではマルチスレッド、マルチプロセッシング、非同期 I/O が同時プログラミングを実現する手段であるとみなされることがありますが、実際には前 2 つも並列プログラミングを実現できます。もちろん、グローバル インタプリタ ロック (GIL) の問題もあります。これについては後で説明します。

マルチスレッドプログラミング

Python 標準ライブラリのthreadingモジュールのクラスはThread、マルチスレッド プログラミングを非常に簡単に実現するのに役立ちます。インターネット経由でファイルをダウンロードする例を使用して、マルチスレッドを使用する場合と使用しない場合の違いを比較してみましょう。コードは次のとおりです。

マルチスレッドのダウンロードは使用しないでください。

#任何问题欢迎+作者v:taosu-ts
import random
import time


def download(*, filename):
    start = time.time()
    print(f'开始下载 {
      
      filename}.')
    time.sleep(random.randint(3, 6))
    print(f'{
      
      filename} 下载完成.')
    end = time.time()
    print(f'下载耗时: {
      
      end - start:.3f}秒.')


def main():
    start = time.time()
    download(filename='Python从入门到住院.pdf')
    download(filename='MySQL从删库到跑路.avi')
    download(filename='Linux从精通到放弃.mp4')
    end = time.time()
    print(f'总耗时: {
      
      end - start:.3f}秒.')


if __name__ == '__main__':
    main()

説明: 上記のコードはネットワーク ダウンロードの機能を実際には実現していませんが、time.sleep()一定期間スリープすることでファイルのダウンロードにかかる時間のオーバーヘッドをシミュレートしており、実際のダウンロード状況と同様です。

上記のコードを実行すると、以下に示すような実行結果が得られます。プログラムにワーカー スレッドが 1 つしかない場合、各ダウンロード タスクは開始する前に前のダウンロード タスクの実行が完了するまで待機する必要があるため、プログラムの合計実行時間はそれぞれの実行時間の合計になることがわかります。 3 つのダウンロード タスクの回数。

开始下载Python从入门到住院.pdf.
Python从入门到住院.pdf下载完成.
下载耗时: 3.005秒.
开始下载MySQL从删库到跑路.avi.
MySQL从删库到跑路.avi下载完成.
下载耗时: 5.006秒.
开始下载Linux从精通到放弃.mp4.
Linux从精通到放弃.mp3下载完成.
下载耗时: 6.007秒.
总耗时: 14.018秒.

実際、上記の 3 つのダウンロード タスク間に論理的な因果関係はありません。これら 3 つは「同時実行」でき、次のダウンロード タスクは前のダウンロード タスクの終了を待つ必要はありません。このために、複数のダウンロード タスクを使用できます。スレッドプログラミングを使用して上記のコードを書き換えます。

import random
import time
from threading import Thread


def download(*, filename):
    start = time.time()
    print(f'开始下载 {
      
      filename}.')
    time.sleep(random.randint(3, 6))
    print(f'{
      
      filename} 下载完成.')
    end = time.time()
    print(f'下载耗时: {
      
      end - start:.3f}秒.')


def main():
    threads = [
        Thread(target=download, kwargs={
    
    'filename': 'Python从入门到住院.pdf'}),
        Thread(target=download, kwargs={
    
    'filename': 'MySQL从删库到跑路.avi'}),
        Thread(target=download, kwargs={
    
    'filename': 'Linux从精通到放弃.mp4'})
    ]
    start = time.time()
    # 启动三个线程
    for thread in threads:
        thread.start()
    # 等待线程结束
    for thread in threads:
        thread.join()
    end = time.time()
    print(f'总耗时: {
      
      end - start:.3f}秒.')


if __name__ == '__main__':
    main()

1回の実行結果は以下の通りです。

开始下载 Python从入门到住院.pdf.
开始下载 MySQL从删库到跑路.avi.
开始下载 Linux从精通到放弃.mp4.
MySQL从删库到跑路.avi 下载完成.
下载耗时: 3.005秒.
Python从入门到住院.pdf 下载完成.
下载耗时: 5.006秒.
Linux从精通到放弃.mp4 下载完成.
下载耗时: 6.003秒.
总耗时: 6.004秒.

上記の実行結果から、プログラム全体の実行時間は、最も長いダウンロード タスクの実行時間とほぼ等しいことがわかります。これは、3 つのダウンロード タスクが同時に実行され、1 つというものは存在しないことを意味します。これにより、プログラムの実行効率が明らかに向上します。簡単に言えば、プログラム内に非常に時間のかかる実行ユニットがあり、これらの時間のかかる実行ユニット間に論理的な因果関係がない場合、つまり、ユニット B の実行がユニット A の実行結果に依存しない場合、次に A と B 2 つのユニットを 2 つの異なるスレッドに配置して、同時に実行できます。この利点は、プログラム実行の待機時間を短縮することに加えて、プログラム内に他のユニットがあるため、ユニットのブロックによってプログラムが「偽装死亡」することがないため、ユーザー エクスペリエンスも向上することです。操作できるもの。

Thread クラスを使用してスレッド オブジェクトを作成する

上記のコードからわかるように、Threadクラスのコンストラクターを使用してスレッド オブジェクトを直接作成でき、start()スレッド オブジェクトのメソッドでスレッドを開始できます。スレッド起動後はtargetパラメータで指定された関数を実行します、もちろんCPUスケジューリングを取得することが前提ですが、target指定されたスレッドで実行する対象の関数にパラメータがある場合はargsパラメータで指定する必要がありますキーワード パラメータの場合は、kwargsパラメータを介して渡すことができます。Threadクラスのコンストラクタには他にも多くのパラメータがありますが、それらについてはその都度説明します。現時点で習得する必要があるのはtargetargsですkwargs

Thread クラスを継承してスレッドをカスタマイズする

上記のコードで示したスレッドの作成方法以外にも、Threadクラスを継承してメソッドを書き換えることでrun()スレッドをカスタマイズすることができます。具体的なコードは以下のとおりです。

import random
import time
from threading import Thread


class DownloadThread(Thread):

    def __init__(self, filename):
        self.filename = filename
        super().__init__()

    def run(self):
        start = time.time()
        print(f'开始下载 {
      
      self.filename}.')
        time.sleep(random.randint(3, 6))
        print(f'{
      
      self.filename} 下载完成.')
        end = time.time()
        print(f'下载耗时: {
      
      end - start:.3f}秒.')


def main():
    threads = [
        DownloadThread('Python从入门到住院.pdf'),
        DownloadThread('MySQL从删库到跑路.avi'),
        DownloadThread('Linux从精通到放弃.mp4')
    ]
    start = time.time()
    # 启动三个线程
    for thread in threads:
        thread.start()
    # 等待线程结束
    for thread in threads:
        thread.join()
    end = time.time()
    print(f'总耗时: {
      
      end - start:.3f}秒.')


if __name__ == '__main__':
    main()

スレッドプールを使用する

タスクを複数のスレッドに配置して、スレッド プールを通じて実行することもできるため、スレッド プールを通じてスレッドを使用することは、マルチスレッド プログラミングにとって理想的な選択肢となります。実際、スレッドの作成と解放には大きなオーバーヘッドがかかるため、スレッドの作成と解放を頻繁に行うことは通常は良い選択ではありません。スレッドプールを利用することで、あらかじめ複数のスレッドを用意しておくことができ、利用時にカスタムコードでスレッドを作成したり解放したりする必要はなく、スレッドプール内のスレッドを直接再利用することができます。Python の組み込みconcurrent.futuresモジュールはスレッド プールのサポートを提供しており、コードは次のとおりです。

import random
import time
from concurrent.futures import ThreadPoolExecutor
from threading import Thread


def download(*, filename):
    start = time.time()
    print(f'开始下载 {
      
      filename}.')
    time.sleep(random.randint(3, 6))
    print(f'{
      
      filename} 下载完成.')
    end = time.time()
    print(f'下载耗时: {
      
      end - start:.3f}秒.')


def main():
    with ThreadPoolExecutor(max_workers=4) as pool:
        filenames = ['Python从入门到住院.pdf', 'MySQL从删库到跑路.avi', 'Linux从精通到放弃.mp4']
        start = time.time()
        for filename in filenames:
            pool.submit(download, filename=filename)
    end = time.time()
    print(f'总耗时: {
      
      end - start:.3f}秒.')


if __name__ == '__main__':
    main()

デーモンスレッド

いわゆる「デーモン スレッド」は、メイン スレッドが終了すると保持する価値のない実行スレッドです。ここに保持する価値がないとは、他のすべての非デーモン スレッドの実行が終了した後にデーモン スレッドが破棄され、現在のプロセス内のすべての非デーモン スレッドが保護されることを意味します。簡単に言うと、デーモン スレッドはメイン スレッドとともにハングアップし、メイン スレッドのライフ サイクルがプロセスのライフ サイクルとなります。理解できない場合は、簡単なコードを見てみましょう。

import time
from threading import Thread


def display(content):
    while True:
        print(content, end='', flush=True)
        time.sleep(0.1)


def main():
    Thread(target=display, args=('Ping', )).start()
    Thread(target=display, args=('Pong', )).start()


if __name__ == '__main__':
    main()

説明: 上記のコードでは、print関数のパラメータをにflush設定していますTrue。これは、flushパラメータの値が でありFalse改行処理が実行されない場合、各出力の内容がオペレーティング システムの出力バッファに配置されるためです。バッファが出力コンテンツで満たされ、出力を生成するためにバッファが空になるまで。上記の現象は、I/Oの中断を減らしCPU使用率を向上させるためにOSが設定したものですが、コードをインタラクティブにするため、出力のたびに出力バッファを強制的にクリアするパラメータを設定していますprintprintflushTrue

コードの実行を手動で中断しない限り、2 つのサブスレッドに無限ループがあるため、上記のコードは実行後に停止しません。ただし、スレッド オブジェクトの作成時にdaemonという名前のパラメータを設定するTrueと、2 つのスレッドはデーモン スレッドになり、他のスレッドが終了すると、無限ループが発生しても、2 つのデーモン スレッドはハングアップし、実行は続行されません。 、コードは次のとおりです。

import time
from threading import Thread


def display(content):
    while True:
        print(content, end='', flush=True)
        time.sleep(0.1)


def main():
    Thread(target=display, args=('Ping', ), daemon=True).start()
    Thread(target=display, args=('Pong', ), daemon=True).start()
    time.sleep(5)


if __name__ == '__main__':
    main()

上記のコードでは、メイン スレッドを 5 秒間スリープさせる行をメイン スレッドに追加しました。このプロセス中、出力と出力time.sleep(5)のデーモン スレッドは、メイン スレッドが 5 秒後に終了するまで実行され続けます。デーモン スレッドも破棄され、実行を継続できなくなります。PingPong

考察: 上記のコードの 12 行目daemon=Trueを削除すると、コードはどのように実行されるでしょうか? 興味のある読者は、それを試して、実際の実行結果が想像と一致するかどうかを確認してください。

資源競争

マルチスレッド コードを作成する場合、複数のスレッドが同じリソース (オブジェクト) をめぐって競合することは避けられません。この場合、競合しているリソースを保護する合理的なメカニズムがない場合、予期しない状況が発生する可能性があります。次のコードは、100同じ銀行口座 (初期残高は元) に送金するスレッドを作成し0、各スレッドの送金金額は1元です。通常の状況では、銀行口座の最終残高は100人民元であるはずですが、次のコードを実行しても人民元の結果を取得できません100

import time

from concurrent.futures import ThreadPoolExecutor


class Account(object):
    """银行账户"""

    def __init__(self):
        self.balance = 0.0

    def deposit(self, money):
        """存钱"""
        new_balance = self.balance + money
        time.sleep(0.01)
        self.balance = new_balance


def main():
    """主函数"""
    account = Account()
    with ThreadPoolExecutor(max_workers=16) as pool:
        for _ in range(100):
            pool.submit(account.deposit, 1)
    print(account.balance)


if __name__ == '__main__':
    main()

上記コードのクラスAccountは銀行口座、depositメソッドは入金動作、パラメータはmoney入金金額を表しており、time.sleep関数シミュレーションによる入金受付には一定の時間がかかります。スレッド プールを介してアカウントに送金するスレッドを開始しましたが、上記のコードでは望ましい結果が100実行されませんでした。これは、複数のスレッドがリソースを求めて競合するときに発生する可能性のあるデータの不整合の問題です。100上記のコードの最初の行に注意してください14。複数のスレッドがこのコード行を実行すると、同じ残高に入金金額を追加する操作が実行され、「更新が失われる」現象が発生します。以前に変更されたデータは、その後の変更によって結果が上書きされるため、正しい結果が得られません。

上記の問題を解決するには、ロック機構を使用して、ロックを通じてデータを操作するキーコードを保護することができます。Python 標準ライブラリのモジュールには、ロック メカニズムをサポートするクラスが用意されています。ここでは 2 つの違いについては詳しく説明しませんので、これらを直接使用することをお勧めthreadingます次に、銀行口座にロック オブジェクトを追加し、そのロック オブジェクトを使用して、先ほど入金したときに発生した「更新が失われる」問題を解決するコードは次のとおりです。LockRLockRLock

import time

from concurrent.futures import ThreadPoolExecutor
from threading import RLock


class Account(object):
    """银行账户"""

    def __init__(self):
        self.balance = 0.0
        self.lock = RLock()

    def deposit(self, money):
        # 获得锁
        self.lock.acquire()
        try:
            new_balance = self.balance + money
            time.sleep(0.01)
            self.balance = new_balance
        finally:
            # 释放锁
            self.lock.release()


def main():
    """主函数"""
    account = Account()
    with ThreadPoolExecutor(max_workers=16) as pool:
        for _ in range(100):
            pool.submit(account.deposit, 1)
    print(account.balance)


if __name__ == '__main__':
    main()

上記のコードでは、ロックの取得と解放の操作は、コンテキスト構文を使用して実装することもできます。コンテキスト構文を使用すると、コードがよりシンプルで洗練されたものになるため、この方法を使用することをお勧めします。

import time

from concurrent.futures import ThreadPoolExecutor
from threading import RLock


class Account(object):
    """银行账户"""

    def __init__(self):
        self.balance = 0.0
        self.lock = RLock()

    def deposit(self, money):
        # 通过上下文语法获得锁和释放锁
        with self.lock:
            new_balance = self.balance + money
            time.sleep(0.01)
            self.balance = new_balance


def main():
    """主函数"""
    account = Account()
    with ThreadPoolExecutor(max_workers=16) as pool:
        for _ in range(100):
            pool.submit(account.deposit, 1)
    print(account.balance)


if __name__ == '__main__':
    main()

考え方: 上記のコードを、銀行口座に入金するスレッドを 5 スレッド、銀行口座からお金を引き出すスレッドを 5 つに変更します。銀行口座の残高が不足している場合、お金を引き出すスレッドは停止し、出金するスレッドを待つ必要があります。お金を入金するためにお金を入金します。もう一度お金を引き出してみてください。ここではスレッド スケジューリングの知識が必要です。threadingモジュール内のConditionクラスを自分で学習して、このタスクを完了できるかどうかを確認してください。

GILの問題

公式の Python インタープリター (通常は CPython と呼ばれます) を使用して Python プログラムを実行する場合、マルチスレッドを使用して CPU 使用率を 400% (4 コア CPU の場合) または 800% (8 コア CPU の場合) 近くまで高めることはできません。 .core CPU)、CPython はコード実行時に GIL(グローバル インタプリタ ロック)によって制限されるためです。具体的には、CPython がコードを実行するとき、対応するスレッドは最初に GIL を取得する必要があり、その後 100 (バイトコード) 命令が実行されるたびに、CPython は GIL を取得したスレッドに積極的に GIL を解放させ、他のスレッドが可能になるようにします。実行します。GIL の存在により、CPU のコア数に関係なく、作成した Python コードが並列実行される可能性はありません。

GIL は、公式 Python インタープリターの設計から残された歴史的な問題です。この問題を解決し、マルチスレッドでマルチコア CPU を活用できるようにするには、GIL を使用せずに Python インタープリターを再実装する必要があります。公式声明によると、この問題は Python バージョン 4.0 がリリースされると解決される予定なので、様子を見ましょう。現時点では、CPython の場合、CPU のマルチコアの利点を最大限に活用したい場合は、マルチプロセスの使用を検討できます。これは、各プロセスが Python インタプリタに対応し、各プロセスが独自の独立した GIL を持っているためです。 GIL.の限界を突破できるとのこと。次の章では、マルチプロセスに関する関連知識を紹介し、マルチスレッドとマルチプロセスのコードと実行効果を比較します。

Guess you like

Origin blog.csdn.net/ml202187/article/details/132035071