top of page
集中できるオススメの音楽です。リラックスしながら学習を進めていきましょう。
オススメ音楽のチャンネルはこちら
執筆者の写真現役社内SEライター

【Python中級編】多重スレッドとマルチプロセスの基本を解説します


【Python中級編】多重スレッドとマルチプロセスの基本を解説します

はじめに


Pythonは多様なプログラミングパラダイムをサポートしていますが、マルチスレッドとマルチプロセスを用いた並列処理もその一つです。

これらの概念を理解し適切に活用することで、プログラムのパフォーマンスを大幅に向上させることが可能です。特に、多くのIO処理を伴うプログラムやCPUを大量に使用するプログラムにおいて、これらの技術は非常に有用です。


スレッドとプロセスの基本的な違い

スレッドとプロセスは、オペレーティングシステムがタスクを並行して実行するための基本的な単位です。

両者の主な違いは、それぞれが持つ状態とリソースにあります。

  • プロセス:プロセスはオペレーティングシステムから独立したメモリ空間とリソースを割り当てられ、その中で一つ以上のスレッドを実行します。プロセス間でのリソース共有は、オペレーティングシステムの管理下で行われ、通常はIPC(Inter-Process Communication)メカニズムを通じて行われます。

  • スレッド:一方、スレッドはプロセス内で実行される軽量な実行単位で、その親プロセスのリソース(メモリ空間、ファイルハンドラなど)を共有します。このため、スレッド間の通信は容易で、同一プロセス内のスレッドは共有メモリを通じてデータをやりとりすることが可能です。

Pythonでのマルチスレッドとマルチプロセスの適用範囲

Pythonはマルチスレッドとマルチプロセスの両方をサポートしていますが、それぞれが最も効果を発揮する状況は異なります。

マルチスレッドはIOバウンドのタスク(ファイル操作、ネットワーク通信など)で最も効果的です。

これらのタスクはCPUの使用率が低く、入出力待ち時間が大部分を占めます。

そのため、同時に複数のIOバウンドタスクをスレッドで並行実行することで、全体の待ち時間を大幅に短縮することが可能です。


一方、マルチプロセスはCPUバウンドのタスク(数値計算、画像処理など)に対して効果的です。

PythonのスレッドはGIL(Global Interpreter Lock)のために同時に1つのスレッドしか実行できないため、CPUバウンドのタスクに対してはマルチスレッドが有効でない場合が多いです。

しかし、各プロセスが独自のメモリ空間とGILを持つため、マルチプロセスは複数のCPUコアをフルに活用することが可能です。


この記事では、どのようにPythonでマルチスレッドとマルチプロセスを使用するかを学んでいきましょう。



目次

  • Pythonでの多重スレッドの理解

  • Pythonでのマルチプロセスの理解

  • スレッドとプロセスの選択:いつ、どこで、どのように使用するか

  • Pythonの並列処理ライブラリ

  • まとめ



Pythonでの多重スレッドの理解


Pythonでは、複数のタスクを同時に実行するためのマルチスレッドプログラミングが可能です。

これは、特にI/O処理が主体のプログラムにおいて、効率的にタスクを処理するための重要な手法となります。しかし、Pythonのマルチスレッドプログラミングは、正しく理解し適用するためには注意が必要な部分もあります。


この章では、Pythonでの多重スレッドの基本的な概念と使い方について詳しく解説します。

スレッドの作成方法、スレッドの安全な利用に必要なロックメカニズム、そしてPythonの特性であるGIL(Global Interpreter Lock)の存在とその影響について学んでいきます。


さらに、具体的なマルチスレッドプログラムの例を通じて、これらの概念がどのように実際のコードに適用されるのかを学びます。


Pythonにおけるマルチスレッドプログラミングの知識は、よりパフォーマンスの高いソフトウェアを開発するための重要な一歩となります。本章を通じて、Pythonのマルチスレッドプログラミングについての理解を深め、その力を自分のプログラムに活かしていきましょう。


スレッドの作成方法

Pythonではthreadingモジュールを用いてスレッドを作成します。

最も簡単な方法はThreadクラスのインスタンスを作成し、そのtarget引数に実行したい関数を指定することです。以下に簡単な例を示します:

import threading

def print_numbers():
    for i in range(10):
        print(i)

def print_letters():
    for letter in "abcdefghij":
        print(letter)

# スレッドの作成
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_letters)

# スレッドの開始
thread1.start()
thread2.start()

# スレッドの完了を待つ
thread1.join()
thread2.join()

このコードでは、数字を表示するスレッドと文字を表示するスレッドを並行に実行します。


スレッドの安全な利用(ロックメカニズム)

スレッドを使用する際には、特に共有リソースへの同時アクセスによる競合状態を避けるためにロックを使用することが重要です。

PythonのthreadingモジュールはLockクラスも提供しており、これを使ってクリティカルセクションを保護することができます。以下に例を示します。

import threading

# 共有リソース
counter = 0
# ロックオブジェクト
lock = threading.Lock()

def increment_counter():
    global counter
    with lock:
        temp = counter + 1
        counter = temp

# スレッドの作成
threads = []
for _ in range(100):
    thread = threading.Thread(target=increment_counter)
    thread.start()
    threads.append(thread)

# スレッドの完了を待つ
for thread in threads:
    thread.join()

print(counter)

ここでは、100個のスレッドが共有変数counterを同時にインクリメントしますが、ロックを用いることで正しく更新できています。


PythonのGIL(Global Interpreter Lock)とその影響

PythonにはGlobal Interpreter Lock(GIL)という仕組みがあります。

GILは一度に1つのスレッドだけがPythonオブジェクトにアクセスできるように制限します。


これは、CPU集約型のマルチスレッドプログラムに対するPythonのパフォーマンスを大きく制限します。

つまり、Pythonのスレッドは同時に複数のCPUコアを使用することができないため、CPU使用率が高いタスクではマルチスレッドはあまり効果的ではありません。


マルチスレッドを利用する具体的な例とコード

それでは、Pythonでのマルチスレッドの利用例を見てみましょう。

例えば、ウェブサイトからデータを非同期にダウンロードするプログラムを考えてみます。

import requests
import threading
import time

# ダウンロードするウェブサイトのリスト
sites = ["https://www.example.com"] * 10

def download_site(site):
    response = requests.get(site)
    print(f"Downloaded {site}")

start_time = time.time()
threads = []

# スレッドを作成し、開始する
for site in sites:
    thread = threading.Thread(target=download_site, args=(site,))
    thread.start()
    threads.append(thread)

# すべてのスレッドが終了するまで待つ
for thread in threads:
    thread.join()

print(f"Downloaded {len(sites)} sites in {time.time() - start_time} seconds")

この例では、各スレッドがウェブサイトを個別にダウンロードします。

これにより、ダウンロードの全体の完了時間が大幅に短縮されます。



Pythonでのマルチプロセスの理解


Pythonでは、処理を並列に実行するための強力な機能としてマルチプロセスが用意されています。

マルチスレッドとは異なり、マルチプロセスはそれぞれのプロセスが独立したメモリ空間を持つため、複数のCPUコアを効率的に利用し、CPU集約的なタスクを高速化することが可能です。


しかし、プロセス間でデータを共有するためには特別な手法が必要となり、それがマルチプロセスプログラミングの一部となります。また、Pythonではプロセスの生成、制御、そしてプロセス間のデータのやり取りを簡単にするための機能が提供されています。


この章では、Pythonでのマルチプロセスプログラミングの基本について解説します。

具体的には、プロセスの作成方法、プロセス間の通信とデータ共有、そして実際のマルチプロセスプログラムの作成とそのコードについて詳しく見ていきます。

これらの知識を身につけることで、Pythonプログラミングの幅が一段と広がります。


プロセスの作成方法

Pythonのmultiprocessingモジュールを使って新たなプロセスを作成することができます。multiprocessing.Processクラスを利用し、そのインスタンスを作成するときにtarget引数に実行したい関数を、args引数にその関数の引数を指定します。

プロセスはstart()メソッドで開始し、join()メソッドで終了を待ちます。


例えば、以下のコードは2つの関数print_squareとprint_cubeをそれぞれ別々のプロセスで実行します。

import multiprocessing

def print_square(number):
    print(f'Square: {number * number}')

def print_cube(number):
    print(f'Cube: {number * number * number}')

# プロセスの作成
process1 = multiprocessing.Process(target=print_square, args=(10,))
process2 = multiprocessing.Process(target=print_cube, args=(10,))

# プロセスの開始
process1.start()
process2.start()

# プロセスの完了を待つ
process1.join()
process2.join()


プロセス間の通信(IPC)とデータ共有

マルチプロセスでは各プロセスが独立したメモリ空間を持つため、プロセス間でデータを共有するためには特別な方法が必要です。

Pythonではmultiprocessing.Queueやmultiprocessing.Pipeを使ってプロセス間でデータをやり取りすることができます。


以下の例では、主プロセスが作成したデータをワーカープロセスが処理します。

データはQueueを通じてやり取りされます。

import multiprocessing

def worker(queue):
    while not queue.empty():
        item = queue.get()
        print(f'Worker: {item}')

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

    # Queueにデータを追加
    for i in range(10):
        queue.put(i)

    # プロセスを作成し、開始
    process = multiprocessing.Process(target=worker, args=(queue,))
    process.start()

    # プロセスの完了を待つ
    process.join()


マルチプロセスを利用する具体的な例とコード

マルチプロセスはCPU集約型のタスクで大いに役立ちます。

以下の例は、マルチプロセスを使って複数の数値の平方根を並列に計算するコードです。

import multiprocessing
import math

# 平方根を計算する関数
def compute_sqrt(numbers, result):
    for idx, number in enumerate(numbers):
        result[idx] = math.sqrt(number)

if __name__ == "__main__":
    # 入力となる数値
    numbers = [4, 9, 16, 25, 36, 49, 64, 81]

    # 結果を格納するための共有メモリ
    result = multiprocessing.Array('d', len(numbers))

    # プロセスを作成し、開始
    process = multiprocessing.Process(target=compute_sqrt, args=(numbers, result))
    process.start()

    # プロセスの完了を待つ
    process.join()

    # 結果を表示
    print(list(result))

この例では、複数の数値の平方根を計算するcompute_sqrt関数が新たなプロセスで実行され、共有メモリresultに結果が書き込まれます。


このように、CPUの全てのコアを利用するためにマルチプロセスを使用することで、計算処理を高速化できます。



スレッドとプロセスの選択:いつ、どこで、どのように使用するか


この章では、Pythonのマルチスレッドとマルチプロセスの適切な使用法について深く掘り下げます。

それぞれの使用法はタスクの種類に大きく依存します。

一般的に、CPU集約型のタスクはマルチプロセスを、IO集約型のタスクはマルチスレッドを使用することが推奨されます。しかし、これは絶対的なルールではなく、具体的な状況や要件により最適な選択は変わります。


この章を通じて、あなたはCPU集約型とIO集約型のタスクが何か、それぞれに最適な並行処理方法は何か、そしてそれらをPythonでどのように実装するかを理解することができます。

また、実際の問題への適用例を通じて、マルチスレッドとマルチプロセスの適用方法を具体的に理解することができます。これらの知識は、あなたが効率的でスケーラブルなPythonプログラムを作成するための重要な基盤となります。


CPU集約型とIO集約型のタスク

Pythonでは、タスクの種類によってマルチスレッドまたはマルチプロセスを選択することが重要です。

一般的に、CPU集約型のタスクではマルチプロセス、IO集約型のタスクではマルチスレッドを使用します。


しかし、この決定はタスクの具体的な内容や要件によります。

例えば、以下のようなCPU集約型のタスクがあります。このタスクは、大きなリストの数値をすべて足し合わせるというものです。

これは、すべての数値を一つずつ処理する必要があり、CPUの能力をフルに利用します。

def cpu_bound(number_list):
    return sum(number_list)

一方、以下のようなIO集約型のタスクもあります。

このタスクは、複数のURLからデータをダウンロードするものです。

各ダウンロードは独立しており、ネットワークの応答を待つ時間が長いため、マルチスレッドを使用することで効率的に処理を行うことができます。

import requests

def download_site(url):
    response = requests.get(url)
    return response.content


スレッドとプロセスの使用例

CPU集約型のタスクの例をさらに詳しく見てみましょう。

前述のcpu_bound関数を使用して、大きなリストを作成し、その合計を求める処理を行います。

マルチプロセスを使用すると、このような処理を並列化して高速化することができます。

from multiprocessing import Pool

def process_data():
    data = [list(range(1000000)) for _ in range(10)]
    with Pool() as p:
        p.map(cpu_bound, data)

一方、IO集約型のタスクではマルチスレッドを使用します。

前述のdownload_site関数を使用して、複数のURLからデータをダウンロードする例を考えてみましょう。

from concurrent.futures import ThreadPoolExecutor

def download_all_sites(sites):
    with ThreadPoolExecutor(max_workers=5) as executor:
        executor.map(download_site, sites)


ケーススタディ:実世界の問題に対するマルチスレッドとマルチプロセスの適用

実世界の問題に対して、これらのテクニックをどのように適用するかを見てみましょう。

たとえば、大量のウェブページをスクレイピングするスクリプトを書いているとします。

各ページのダウンロードは独立しており、ネットワークからの応答を待つ時間が長いため、マルチスレッドを使用します。

sites = [
    "https://www.example.com",
    "https://www.example.org",
    "https://www.example.net",
    # 他のウェブサイト...
]

download_all_sites(sites)

しかしここで、スクレイピングしたデータを処理するためのCPU集約型のタスクが追加されたとしましょう。この場合、スレッドを使用してダウンロードを並列化し、さらにダウンロードしたデータの処理をプロセスで並列化することで、全体の処理を高速化することができます。



Pythonの並列処理ライブラリ


Pythonには、threading、multiprocessing、およびconcurrent.futuresなど、並列処理を支援するための強力な標準ライブラリが含まれています。

これらのライブラリを利用することで、開発者は複雑なスレッドやプロセスの管理から解放され、タスクの並行実行に集中することができます。


この章では、これらのライブラリの基本的な使い方と違いを、具体的なコード例を交えて説明します。


threadingモジュールとmultiprocessingモジュールの比較

threadingモジュールはPythonの基本的なスレッド管理機能を提供します。

このモジュールを使うと、複数のスレッドを生成し、それぞれに異なるタスクを実行させることができます。例えば、以下のコードは5つのスレッドを生成し、それぞれでworker関数を実行しています。

import threading

def worker(num):
    print('Worker: %s' % num)

for i in range(5):
    threading.Thread(target=worker, args=(i,)).start()

しかし、PythonのGILにより、実際にはこれらのスレッドが同時に実行されることはありません。

一方、multiprocessingモジュールは新しいプロセスを生成し、それぞれのプロセスでPythonのインタープリタを実行します。

これにより、GILの制限を受けずに、マルチコアプロセッサを活用することができます。

以下のコードは、5つのプロセスを生成し、それぞれでworker関数を実行しています。

from multiprocessing import Process

def worker(num):
    print('Worker: %s' % num)

for i in range(5):
    Process(target=worker, args=(i,)).start()


concurrent.futuresモジュールの紹介

concurrent.futuresモジュールは、マルチスレッドとマルチプロセスをより高レベルで抽象化したAPIを提供します。このモジュールにはThreadPoolExecutorとProcessPoolExecutorの2つのエクゼキュータクラスが含まれています。

これらのエクゼキュータを使用すると、複数のタスクを生成してそれらを並行して実行させることが容易になります。

以下の例では、10個のタスクをThreadPoolExecutorに投げて、並行に実行させています。

from concurrent.futures import ThreadPoolExecutor

def worker(num):
    return num * num

with ThreadPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(worker, range(10)))

print(results)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]


ライブラリを使用したマルチスレッドとマルチプロセスのコード例

concurrent.futuresモジュールを使用すると、マルチスレッドやマルチプロセスのコードをよりシンプルに書くことができます。

以下の例は、先ほどのマルチスレッドのコードをconcurrent.futuresを使用して書き直したものです。

from concurrent.futures import ThreadPoolExecutor

def worker(num):
    print('Worker: %s' % num)

with ThreadPoolExecutor(max_workers=5) as executor:
    for i in range(5):
        executor.submit(worker, i)

同様に、ProcessPoolExecutorを使用すれば、マルチプロセスのコードを簡潔に書くことができます。

from concurrent.futures import ProcessPoolExecutor

def worker(num):
    print('Worker: %s' % num)

with ProcessPoolExecutor(max_workers=5) as executor:
    for i in range(5):
        executor.submit(worker, i)

このようにconcurrent.futuresモジュールを使うことで、マルチスレッドとマルチプロセスの操作を高レベルで行うことができ、コードの可読性や保守性が向上します。



まとめ


この記事で学んだことのまとめ

本記事では、Pythonでの多重スレッドとマルチプロセスの基本について学びました。

スレッドとプロセスの基本的な違い、それぞれの適用範囲、そしてそれぞれの具体的な使用方法について説明しました。また、PythonのGILによる制約とその回避方法についても触れました。

最後に、Pythonの並列処理を支援するためのライブラリ、特にthreading、multiprocessing、concurrent.futuresモジュールの使い方とその便利さについて学びました。

これらの知識を活用すれば、Pythonでの並列処理を効果的に扱うことができます。


一方で、これらは並列処理の一部に過ぎません。スレッドやプロセスだけでなく、イベント駆動型プログラミングや非同期I/Oなど、さらなるパラダイムも存在します。


また、次の記事ではWebスクレイピングの導入について解説していきます。

これらの学習を経て、Pythonでの高性能な並列処理とデータ取得の技術を身につけてください。

これらの知識は、大規模なデータ解析、マシンラーニングのタスク、または高負荷なWebサービスを扱う際に非常に有用です。


Copyright Tech School Navi. All Right Reserved.

bottom of page