ホームページ >バックエンド開発 >Python チュートリアル >Python でのスレッド化とマルチプロセッシングによる同時実行性

Python でのスレッド化とマルチプロセッシングによる同時実行性

DDD
DDDオリジナル
2024-09-14 06:23:121094ブラウズ

Concurrency in Python with Threading and Multiprocessing

同時実行性は、アプリケーションのパフォーマンスを向上させるために複数のタスクを同時に実行できるようにする、現代のプログラミングにおける重要な概念です。

Python で同時実行性を実現するにはいくつかの方法がありますが、スレッド化とマルチプロセッシングが最もよく知られています。

この記事では、これら 2 つのメソッドを詳しく調査し、それらがどのように機能するかを理解し、実際のコード例とともに、それぞれをいつ使用するかについて説明します。


同時実行とは何ですか?

スレッド化とマルチプロセッシングについて話す前に、同時実行性の意味を理解することが重要です。

同時実行性とは、プログラムが複数のタスクまたはプロセスを同時に実行できることです。

これにより、特にファイルの読み取りや大量の計算などを行う必要がある場合に、プログラムのリソース使用率が向上し、実行速度が向上します。

同時実行性を実現するには、主に 2 つの方法があります:

  • 並列処理: コンピューターのプロセッサの異なる部分で複数のタスクを同時に実行すること。
  • 同時実行性: 同じ期間内に複数のタスクを処理しますが、必ずしもまったく同じ瞬間に処理するとは限りません。

Python では、同時実行性を実現する 2 つの主な方法が提供されています。

  • スレッド化: 同時に管理できるタスク用。
  • マルチプロセッシング: 異なるプロセッサ コア上で真に同時に実行する必要があるタスク用。

Python でのスレッド化

スレッド化を使用すると、スレッドと呼ばれる複数の小さなプロセス単位を同じプロセス内で実行し、同じメモリ空間を共有できます。

スレッドはプロセスよりも軽く、スレッド間の切り替えは高速です。

ただし、Python でのスレッド処理はグローバル インタープリター ロック (GIL) の対象となり、一度に 1 つのスレッドのみが Python コードを実行できるようになります。

スレッディングの仕組み

Python のスレッド モジュールは、スレッドを作成および管理するためのシンプルかつ柔軟な方法を提供します。

基本的な例から始めましょう:

import threading
import time


def print_numbers():
    for i in range(5):
        print(f"Number: {i}")
        time.sleep(1)


# Creating a thread
thread = threading.Thread(target=print_numbers)

# Starting the thread
thread.start()

# Wait for the thread to complete
thread.join()

print("Thread has finished executing")


# Output:
# Number: 0
# Number: 1
# Number: 2
# Number: 3
# Number: 4
# Thread has finished executing

この例では:

  • 0 から 4 までの数字を 1 秒の遅延で出力する関数 print_numbers() を定義します。
  • threading.Thread() を使用してスレッドを作成し、ターゲット関数として print_numbers() を渡します。
  • start() メソッドはスレッドの実行を開始し、join() メソッドはメイン プログラムがスレッドの終了を待ってから続行します。

例: I/O バウンドタスクのスレッド化

スレッド処理は、ファイル操作、ネットワーク リクエスト、データベース クエリなど、プログラムが外部リソースの待機にほとんどの時間を費やす I/O バウンドのタスクに特に役立ちます。

スレッドを使用してファイルのダウンロードをシミュレートする例を次に示します:

import threading
import time


def download_file(file_name):
    print(f"Starting download of {file_name}...")
    time.sleep(2)  # Simulate download time
    print(f"Finished downloading {file_name}")


files = ["file1.zip", "file2.zip", "file3.zip"]

threads = []

# Create and start threads
for file in files:
    thread = threading.Thread(target=download_file, args=(file,))
    thread.start()
    threads.append(thread)

# Ensure all threads have finished
for thread in threads:
    thread.join()

print("All files have been downloaded.")

# Output:
# Starting download of file1.zip...
# Starting download of file2.zip...
# Starting download of file3.zip...
# Finished downloading file1.zip
# Finished downloading file2.zip
# Finished downloading file3.zip
# All files have been downloaded.

ファイルのダウンロードごとに個別のスレッドを作成して管理することにより、プログラムは複数のタスクを同時に処理でき、全体的な効率が向上します。

コード内の主要な手順は次のとおりです:

  • 関数 download_file は、ダウンロード プロセスをシミュレートするために定義されています。
  • ダウンロードする必要があるファイルを表すファイル名のリストが作成されます。
  • リスト内のファイルごとに、download_file をターゲット関数として新しいスレッドが作成されます。各スレッドは作成直後に開始され、スレッドのリストに追加されます。
  • メイン プログラムは、すべてのスレッドが join() メソッドの使用を完了するまで待機し、すべてのダウンロードが完了するまでプログラムが続行されないようにします。

スレッド化の制限

スレッド化により I/O バウンドのタスクのパフォーマンスが向上しますが、次のような制限があります。

  • グローバル インタプリタ ロック (GIL): GIL は、CPU バウンドのタスクの実行を一度に 1 つのスレッドに制限し、マルチコア プロセッサでのスレッドの有効性を制限します。
  • 競合状態: スレッドは同じメモリ空間を共有するため、不適切な同期により競合状態が発生する可能性があり、プログラムの結果がスレッドのタイミングに依存します。
  • デッドロック: スレッドがリソースの解放を互いに待機していると、進行が進まないデッドロックが発生する可能性があります。

Python でのマルチプロセッシング

マルチプロセッシングは、スレッドの代わりに別のプロセスを使用することで、スレッドの制限に対処します。

各プロセスには独自のメモリ空間と Python インタープリターがあり、マルチコア システムでの真の並列処理が可能になります。

これにより、マルチプロセッシングは大量の計算を必要とするタスクに最適になります。

マルチプロセッシングの仕組み

Python のマルチプロセッシング モジュールを使用すると、プロセスを簡単に作成および管理できます。

Let’s start with a basic example:

import multiprocessing
import time


def print_numbers():
    for i in range(5):
        print(f"Number: {i}")
        time.sleep(1)


if __name__ == "__main__":
    # Creating a process
    process = multiprocessing.Process(target=print_numbers)

    # Starting the process
    process.start()

    # Wait for the process to complete
    process.join()

    print("Process has finished executing")

# Output:
# Number: 0
# Number: 1
# Number: 2
# Number: 3
# Number: 4
# Process has finished executing

This example is similar to the threading example, but with processes.

Notice that the process creation and management are similar to threading, but because processes run in separate memory spaces, they are truly concurrent and can run on different CPU cores.

Example: Multiprocessing for CPU-Bound Tasks

Multiprocessing is particularly beneficial for tasks that are CPU-bound, such as numerical computations or data processing.

Here’s an example that calculates the square of numbers using multiple processes:

import multiprocessing


def compute_square(number):
    return number * number


if __name__ == "__main__":
    numbers = [1, 2, 3, 4, 5]

    # Create a pool of processes
    with multiprocessing.Pool() as pool:
        # Map function to numbers using multiple processes
        results = pool.map(compute_square, numbers)

    print("Squares:", results)

# Output:
# Squares: [1, 4, 9, 16, 25]

Here are the key steps in the code:

  • A function compute_square is defined to take a number as input and return its square.
  • The code within the if name == "main": block ensures that it runs only when the script is executed directly.
  • A list of numbers is defined, which will be squared.
  • A pool of worker processes is created using multiprocessing.Pool().
  • The map method is used to apply the compute_square function to each number in the list, distributing the workload across multiple processes.

Inter-Process Communication (IPC)

Since each process has its own memory space, sharing data between processes requires inter-process communication (IPC) mechanisms.

The multiprocessing module provides several tools for IPC, such as Queue, Pipe, and Value.

Here’s an example using Queue to share data between processes:

import multiprocessing


def worker(queue):
    # Retrieve and process data from the queue
    while not queue.empty():
        item = queue.get()
        print(f"Processing {item}")


if __name__ == "__main__":
    queue = multiprocessing.Queue()

    # Add items to the queue
    for i in range(10):
        queue.put(i)

    # Create a pool of processes to process the queue
    processes = []
    for _ in range(4):
        process = multiprocessing.Process(target=worker, args=(queue,))
        processes.append(process)
        process.start()

    # Wait for all processes to complete
    for process in processes:
        process.join()

    print("All processes have finished.")


# Output:
# Processing 0
# Processing 1
# Processing 2
# Processing 3
# Processing 4
# Processing 5
# Processing 6
# Processing 7
# Processing 8
# Processing 9
# All processes have finished.

In this example:

  • def worker(queue): Defines a function worker that takes a queue as an argument. The function retrieves and processes items from the queue until it is empty.
  • if name == "main":: Ensures that the following code runs only if the script is executed directly, not if it is imported as a module.
  • queue = multiprocessing.Queue(): Creates a queue object for inter-process communication.
  • for i in range(10): queue.put(i): Adds items (numbers 0 through 9) to the queue.
  • processes = []: Initializes an empty list to store process objects.
  • The for loop for _ in range(4): Creates four worker processes.
  • process = multiprocessing.Process(target=worker, args=(queue,)): Creates a new process with worker as the target function and passes the queue as an argument.
  • processes.append(process): Adds the process object to the processes list.
  • process.start(): Starts the process.
  • The for loop for process in processes: Waits for each process to complete using the join() method.

Challenges of Multiprocessing

While multiprocessing provides true parallelism, it comes with its own set of challenges:

  • Higher Overhead: Creating and managing processes is more resource-intensive than threads due to separate memory spaces.
  • Complexity: Communication and synchronization between processes are more complex than threading, requiring IPC mechanisms.
  • Memory Usage: Each process has its own memory space, leading to higher memory usage compared to threading.

When to Use Threading vs. Multiprocessing

Choosing between threading and multiprocessing depends on the type of task you're dealing with:

Use Threading:

  • For tasks that involve a lot of waiting, such as network operations or reading/writing files (I/O-bound tasks).
  • When you need to share memory between tasks and can manage potential issues like race conditions.
  • For lightweight concurrency without the extra overhead of creating multiple processes.

Use Multiprocessing:

  • For tasks that require heavy computations or data processing (CPU-bound tasks) and can benefit from running on multiple CPU cores at the same time.
  • When you need true parallelism and the Global Interpreter Lock (GIL) in threading becomes a limitation.
  • For tasks that can run independently and don’t require frequent communication or shared memory.

Conclusion

Concurrency in Python is a powerful way to make your applications run faster.

Threading is great for tasks that involve a lot of waiting, like network operations or reading/writing files, but it's not as effective for tasks that require heavy computations because of something called the Global Interpreter Lock (GIL).

On the other hand, multiprocessing allows for true parallelism, making it perfect for CPU-intensive tasks, although it comes with higher overhead and complexity.

データの処理、複数のネットワーク リクエストの処理、複雑な計算のいずれの場合でも、Python のスレッドおよびマルチプロセッシング ツールは、プログラムを可能な限り効率的かつ高速にするために必要なものを提供します。

以上がPython でのスレッド化とマルチプロセッシングによる同時実行性の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。