Pythonのスレッド

こんにちは。プラットフォーム事業部のPです。
本稿ではPythonのスレッド(thread)について紹介させていただきます。

この記事は以下の方を対象としています。

★4 Python開発経験が3年以上。
★3 Python開発経験が1年以上
★2 Python 初級者。簡単なプログラムコードが書けます。
★1 プログラミング未経験。

マルチスレッドとは?

まずスレッド(thread)とは、パソコンのCPUが同時に処理できる命令の実行単位を表します。パソコンのスペックによく書かれているスレッド数はCPUが同時に処理できる命令の数になります。 つまり、マルチスレッドは複数のスレッドが実行されることを意味します。

並行と並列

パソコンの同時作業(multi task)には並行と並列の2種類があります。
並行(Concurrency)は複数の作業を短い間隔で切り替えながら処理することを意味します。
人間が意識できない短い時間で切り替えを反復するため、複数の作業を同時に行えているかのように見えます。 対して並列(Parallelism)は本当に複数の作業を同時に行うことを意味します。

(P製作:image)

Pythonのスレッドと標準モジュール

PythonにはGIL(Global Interpreter Lock)と言う仕組みが存在しており、このGILによりPythonは一度に一つのスレッドしか実行できません。 GILはスレッドを制限することでメモリー管理を安全かつ簡潔にできることが利点ですが、真の意味での並列処理が出来なくなると言う致命的なデメリットがあります。
(GILはI/O処理など一部の作業においては自動的に開放されます)
標準モジュールであるthreadingはPythonのマルチスレッド実装のためのモジュールとして有名ですが、このthreadingは並行(Concurrency)に当たります。

  • the threading module operates within a single process, meaning that all threads share the same memory space. However, the GIL limits the performance gains of threading when it comes to CPU-bound tasks, as only one thread can execute Python bytecode at a time. Despite this, threads remain a useful tool for achieving concurrency in many scenarios.

    (P翻訳) threadingモジュールはシングルプロセスとして動作し、全てのスレッドは同じメモリーを共有します。ただし、CPUバウンドなタスクの場合、GILの制限によりPythonバイトコードを実行できるスレッドは一つだけになります。そんな特徴がありますが、それでもスレッドは並行処理を実現したい時にとても有用です。
    - 引用: From Python公式Doc(参考文献No.2)

threadingモジュールの使い方

Pythonコードは一般的には書かれた順、上から下まで順番に実行されます。

例えば

print(1)
print(2)
print(3)

のようなコードは

1
2
3

のような結果になります。 しかし、threadingモジュールを利用すれば並行処理でありますが、同時に動作するかのような処理を実現できます。

ケース1 : threadingモジュールあり

import threading
def task1():
    print("task1 開始!")
    for i in range(90000000):
        i+=1
        if i % 30000000 == 0:
            print(f" task1 実行中 {i//30000000}/4")
    print("task1 終わり!")

def task2():
    print("task2 開始!")
    for i in range(90000000):
        i+=1
        if i % 30000000 == 0:
            print(f" task2 実行中 {i//30000000}/4")
    print("task2 終わり!")

def threadtest():
    thread1 = threading.Thread(target=task1)
    thread2 = threading.Thread(target=task2)
    
    thread1.start()
    thread2.start()

threadtest()

上記の実行結果は下記となります。

  • task1 開始!
  • task2 開始!
    • task2 実行中 1/4
    • task1 実行中 1/4
    • task2 実行中 2/4
    • task1 実行中 2/4
    • task1 実行中 3/4
  • task1 終わり!
    • task2 実行中 3/4
  • task2 終わり!

次に、threadingモジュールを使わずに関数を実行してみましょう。

ケース2 : threadingモジュールなし

def task1():
    print("task1 開始!")
    for i in range(90000000):
        i+=1
        if i % 30000000 == 0:
            print(f" task1 実行中 {i//30000000}/4")
    print("task1 終わり!")

def task2():
    print("task2 開始!")
    for i in range(90000000):
        i+=1
        if i % 30000000 == 0:
            print(f" task2 実行中 {i//30000000}/4")
    print("task2 終わり!")

def normaltest():
    task1()
    task2()

実行結果です。

  • task1 開始!
    •  task1 実行中 1/4
    •  task1 実行中 2/4
    •  task1 実行中 3/4
  • task1 終わり!
  • task2 開始!
    •  task2 実行中 1/4
    •  task2 実行中 2/4
    •  task2 実行中 3/4
  • task2 終わり!

二つのタスクが並行して行われていたケース1と違い、ケース2ではtask1が終了してからtask2が始まることが分かります。

まとめ

今回はthreadingモジュールを用いてPythonのマルチスレッドを実現する方法についてご紹介させていただきました。
並列か並行かの観点で見るとPythonは不便な言語のように見えますが、昨今はGILなしでPythonを実行できるようにするPython free threading研究が有志のエンジニアたちにより行われ、PEP703では試験的に導入されるまでに至りました。
AIの発展やデータ統計などで有用なPythonがGILの開放により活躍できる場面が益々増えることが期待できます。

最後までお読みいただき、ありがとうございました。

参考文献
1.oxylabs | Concurrency vs Parallelism: The Main Differences
2.Python公式ドキュメント - threading
3.PEP 703 – Making the Global Interpreter Lock Optional in CPython