はじめに
Python スレッドの完了を待つ方法を習得することは、堅牢で信頼性の高いアプリケーションを構築するために不可欠です。マルチスレッドプログラムでは、適切な同期により、操作が正しい順序で完了し、リソースが効率的に使用されることが保証されます。
この実験(Lab)では、Python スレッドを作成し、それらの完了を待ち、複数のスレッドを処理する方法を学びます。これらのスキルは、適切な同期を維持しながら、複数のタスクを同時に実行できる並行アプリケーションを開発するための基礎となります。
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
Python スレッドの完了を待つ方法を習得することは、堅牢で信頼性の高いアプリケーションを構築するために不可欠です。マルチスレッドプログラムでは、適切な同期により、操作が正しい順序で完了し、リソースが効率的に使用されることが保証されます。
この実験(Lab)では、Python スレッドを作成し、それらの完了を待ち、複数のスレッドを処理する方法を学びます。これらのスキルは、適切な同期を維持しながら、複数のタスクを同時に実行できる並行アプリケーションを開発するための基礎となります。
Python の threading
モジュールは、スレッドを簡単に作成および管理する方法を提供します。このステップでは、基本的なスレッドを作成し、その動作を観察する方法を学びます。
スレッドは、プログラム内の個別の実行フローです。Python スクリプトを実行すると、メインスレッドと呼ばれる単一のスレッドから開始されます。追加のスレッドを作成することにより、プログラムは複数のタスクを同時に実行できます。
スレッドは、以下の場合に役立ちます。
スレッドを作成して開始する方法を示す簡単な Python スクリプトを作成することから始めましょう。
「File」メニューをクリックし、「New File」を選択して、エディターで新しいファイルを開き、/home/labex/project
ディレクトリに simple_thread.py
として保存します。
次のコードをファイルに追加します。
import threading
import time
def print_numbers():
"""Function that prints numbers from 1 to 5 with a delay."""
for i in range(1, 6):
print(f"Number {i} from thread")
time.sleep(1) ## Sleep for 1 second
## Create a thread that targets the print_numbers function
number_thread = threading.Thread(target=print_numbers)
## Start the thread
print("Starting the thread...")
number_thread.start()
## Main thread continues execution
print("Main thread continues to run...")
print("Main thread is doing other work...")
## Sleep for 2 seconds to demonstrate both threads running concurrently
time.sleep(2)
print("Main thread finished its work!")
Ctrl+S
を押すか、「File」>「Save」をクリックしてファイルを保存します。
ターミナルを開き(まだ開いていない場合)、次を実行してスクリプトを実行します。
python3 /home/labex/project/simple_thread.py
次のような出力が表示されるはずです。
Starting the thread...
Main thread continues to run...
Main thread is doing other work...
Number 1 from thread
Number 2 from thread
Main thread finished its work!
Number 3 from thread
Number 4 from thread
Number 5 from thread
この例では、次のことが行われました。
threading
モジュールと time
モジュールをインポートしました。print_numbers()
を定義しました。target
パラメータを使用して、実行する関数を指定して、スレッドオブジェクトを作成しました。start()
メソッドを使用してスレッドを開始しました。メインスレッドは、数字スレッドがすべての数字を出力する前に終了したことに注意してください。これは、スレッドが独立して実行され、デフォルトでは、他のスレッドがまだ実行されていても、メインスレッドが終了すると Python プログラムが終了するためです。
次のステップでは、join()
メソッドを使用してスレッドの完了を待つ方法を学びます。
前のステップでは、メインスレッドとは独立して実行されるスレッドを作成しました。ただし、プログラムの残りの部分に進む前に、スレッドがその作業を完了するのを待つ必要がある状況が数多くあります。ここで join()
メソッドが役立ちます。
スレッドオブジェクトの join()
メソッドは、join()
メソッドが呼び出されたスレッドが終了するまで、呼び出し元のスレッド(通常はメインスレッド)をブロックします。これは、以下の場合に不可欠です。
join()
メソッドを使用してスレッドの完了を待つ方法を示すために、前の例を変更してみましょう。
/home/labex/project
ディレクトリに join_thread.py
という名前の新しいファイルを作成します。
次のコードをファイルに追加します。
import threading
import time
def calculate_sum(numbers):
"""Function that calculates the sum of numbers with a delay."""
print("Starting the calculation...")
time.sleep(3) ## Simulate a time-consuming calculation
result = sum(numbers)
print(f"Calculation result: {result}")
return result
## Create a list of numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
## Create a thread that targets the calculate_sum function
calculation_thread = threading.Thread(target=calculate_sum, args=(numbers,))
## Start the thread
print("Main thread: Starting the calculation thread...")
calculation_thread.start()
## Do some other work in the main thread
print("Main thread: Doing some other work while waiting...")
for i in range(5):
print(f"Main thread: Working... ({i+1}/5)")
time.sleep(0.5)
## Wait for the calculation thread to complete
print("Main thread: Waiting for the calculation thread to finish...")
calculation_thread.join()
print("Main thread: Calculation thread has finished!")
## Continue with the main thread
print("Main thread: Continuing with the rest of the program...")
python3 /home/labex/project/join_thread.py
次のような出力が表示されるはずです。
Main thread: Starting the calculation thread...
Starting the calculation...
Main thread: Doing some other work while waiting...
Main thread: Working... (1/5)
Main thread: Working... (2/5)
Main thread: Working... (3/5)
Main thread: Working... (4/5)
Main thread: Working... (5/5)
Main thread: Waiting for the calculation thread to finish...
Calculation result: 55
Main thread: Calculation thread has finished!
Main thread: Continuing with the rest of the program...
この例では、次のことが行われました。
calculation_thread.join()
を呼び出しました。join()
メソッドにより、メインスレッドは計算スレッドが終了するまで待機しました。このパターンは、プログラムの残りの部分に進む前に、すべてのスレッド化されたタスクが完了していることを確認する必要がある場合に非常に役立ちます。join()
がないと、メインスレッドは、ワーカー(worker)スレッドがタスクを完了する前に続行し、終了することさえあります。
場合によっては、スレッドを待ちたいが、無期限に待機したくない場合があります。join()
メソッドは、待機する最大秒数を指定するオプションのタイムアウトパラメータを受け入れます。
これを実証するために、コードを変更してみましょう。
/home/labex/project
ディレクトリに join_timeout.py
という名前の新しいファイルを作成します。
次のコードを追加します。
import threading
import time
def long_running_task():
"""A function that simulates a very long-running task."""
print("Long-running task started...")
time.sleep(10) ## Simulate a 10-second task
print("Long-running task completed!")
## Create and start the thread
task_thread = threading.Thread(target=long_running_task)
task_thread.start()
## Wait for the thread to complete, but only for up to 3 seconds
print("Main thread: Waiting for up to 3 seconds...")
task_thread.join(timeout=3)
## Check if the thread is still running
if task_thread.is_alive():
print("Main thread: The task is still running, but I'm continuing anyway!")
else:
print("Main thread: The task has completed within the timeout period.")
## Continue with the main thread
print("Main thread: Continuing with other operations...")
## Let's sleep a bit to see the long-running task complete
time.sleep(8)
print("Main thread: Finished.")
python3 /home/labex/project/join_timeout.py
出力は次のようになります。
Long-running task started...
Main thread: Waiting for up to 3 seconds...
Main thread: The task is still running, but I'm continuing anyway!
Main thread: Continuing with other operations...
Long-running task completed!
Main thread: Finished.
この例では、メインスレッドは、タスクスレッドが完了するまで最大 3 秒間待ちます。タスクに 10 秒かかるため、メインスレッドはタイムアウト後に続行し、タスクスレッドはバックグラウンドで実行を続けます。
このアプローチは、スレッドに完了する機会を与えたいが、一定の時間が経過した後に何があっても続行する必要がある場合に役立ちます。
実際のアプリケーションでは、多くの場合、複数のスレッドを同時に操作する必要があります。このステップでは、Python で複数のスレッドを作成、管理、および同期する方法を学びます。
複数の同様のタスクを扱う場合、それらを同時に処理するために複数のスレッドを作成するのが一般的です。これは、特にファイルのダウンロードやネットワークリクエストなどの I/O バウンド操作の場合、パフォーマンスを大幅に向上させることができます。
複数のスレッドを使用してタスクのリストを処理する例を作成しましょう。
/home/labex/project
ディレクトリに multiple_threads.py
という名前の新しいファイルを作成します。
次のコードを追加します。
import threading
import time
import random
def process_task(task_id):
"""Function to process a single task."""
print(f"Starting task {task_id}...")
## Simulate variable processing time
processing_time = random.uniform(1, 3)
time.sleep(processing_time)
print(f"Task {task_id} completed in {processing_time:.2f} seconds.")
return task_id
## List of tasks to process
tasks = list(range(1, 6)) ## Tasks with IDs 1 through 5
## Create a list to store our threads
threads = []
## Create and start a thread for each task
for task_id in tasks:
thread = threading.Thread(target=process_task, args=(task_id,))
threads.append(thread)
print(f"Created thread for task {task_id}")
thread.start()
print(f"All {len(threads)} threads have been started")
## Wait for all threads to complete
for thread in threads:
thread.join()
print("All tasks have been completed!")
python3 /home/labex/project/multiple_threads.py
出力は、ランダムな処理時間のために毎回異なりますが、次のようなものになるはずです。
Created thread for task 1
Starting task 1...
Created thread for task 2
Starting task 2...
Created thread for task 3
Starting task 3...
Created thread for task 4
Starting task 4...
Created thread for task 5
Starting task 5...
All 5 threads have been started
Task 1 completed in 1.23 seconds.
Task 3 completed in 1.45 seconds.
Task 2 completed in 1.97 seconds.
Task 5 completed in 1.35 seconds.
Task 4 completed in 2.12 seconds.
All tasks have been completed!
この例では、次のことが行われました。
process_task()
関数を定義しました。join()
を使用して、各スレッドが完了するのを待ちました。このパターンは、並行して処理できる一連の独立したタスクがある場合に非常に役立ちます。
より高度なスレッド管理のために、Python の concurrent.futures
モジュールは ThreadPoolExecutor
クラスを提供します。これは、再利用できるワーカー(worker)スレッドのプールを作成します。これは、各タスクに対してスレッドを作成および破棄するよりも効率的です。
スレッドプールを使用して例を書き直しましょう。
/home/labex/project
ディレクトリに thread_pool.py
という名前の新しいファイルを作成します。
次のコードを追加します。
import concurrent.futures
import time
import random
def process_task(task_id):
"""Function to process a single task."""
print(f"Starting task {task_id}...")
## Simulate variable processing time
processing_time = random.uniform(1, 3)
time.sleep(processing_time)
print(f"Task {task_id} completed in {processing_time:.2f} seconds.")
return f"Result of task {task_id}"
## List of tasks to process
tasks = list(range(1, 6)) ## Tasks with IDs 1 through 5
## Create a ThreadPoolExecutor
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
## Submit all tasks and store the Future objects
print(f"Submitting {len(tasks)} tasks to the thread pool with 3 workers...")
future_to_task = {executor.submit(process_task, task_id): task_id for task_id in tasks}
## As each task completes, get its result
for future in concurrent.futures.as_completed(future_to_task):
task_id = future_to_task[future]
try:
result = future.result()
print(f"Got result from task {task_id}: {result}")
except Exception as e:
print(f"Task {task_id} generated an exception: {e}")
print("All tasks have been processed!")
python3 /home/labex/project/thread_pool.py
出力は、ランダムな処理時間のために再び異なりますが、次のようなものになるはずです。
Submitting 5 tasks to the thread pool with 3 workers...
Starting task 1...
Starting task 2...
Starting task 3...
Task 2 completed in 1.15 seconds.
Starting task 4...
Got result from task 2: Result of task 2
Task 1 completed in 1.82 seconds.
Starting task 5...
Got result from task 1: Result of task 1
Task 3 completed in 2.25 seconds.
Got result from task 3: Result of task 3
Task 4 completed in 1.45 seconds.
Got result from task 4: Result of task 4
Task 5 completed in 1.67 seconds.
Got result from task 5: Result of task 5
All tasks have been processed!
スレッドプールアプローチには、いくつかの利点があります。
この例では、max_workers=3
を設定しました。つまり、5 つのタスクがある場合でも、一度に 3 つのスレッドのみが実行されます。スレッドがタスクを完了すると、残りのタスクに再利用されます。
スレッドプールは、同時に実行するスレッドよりも多くのタスクがある場合、またはタスクが継続的に生成されている場合に特に役立ちます。
この最終ステップでは、スレッド管理における 2 つの重要な概念、つまりタイムアウトの設定とデーモンスレッドの使用について学びます。これらのテクニックにより、スレッドの動作とメインプログラムとの対話をより詳細に制御できます。
ステップ 2 で学習したように、join()
メソッドはタイムアウトパラメータを受け入れます。これは、スレッドの完了を待ちたいが、特定の時点までのみ待機したい場合に役立ちます。
タイムアウトを使用してデータを取得しようとする関数を実装する、より実用的な例を作成しましょう。
/home/labex/project
ディレクトリに thread_with_timeout.py
という名前の新しいファイルを作成します。
次のコードを追加します。
import threading
import time
import random
def fetch_data(data_id):
"""Simulate fetching data that might take varying amounts of time."""
print(f"Fetching data #{data_id}...")
## Simulate different fetch times, occasionally very long
fetch_time = random.choices([1, 8], weights=[0.8, 0.2])[0]
time.sleep(fetch_time)
if fetch_time > 5: ## Simulate a slow fetch
print(f"Data #{data_id}: Fetch took too long!")
return None
else:
print(f"Data #{data_id}: Fetch completed in {fetch_time} seconds!")
return f"Data content for #{data_id}"
def fetch_with_timeout(data_id, timeout=3):
"""Fetch data with a timeout."""
result = [None] ## Using a list to store result from the thread
def target_func():
result[0] = fetch_data(data_id)
## Create and start the thread
thread = threading.Thread(target=target_func)
thread.start()
## Wait for the thread with a timeout
thread.join(timeout=timeout)
if thread.is_alive():
print(f"Data #{data_id}: Fetch timed out after {timeout} seconds!")
return "TIMEOUT"
else:
return result[0]
## Try to fetch several pieces of data
for i in range(1, 6):
print(f"\nAttempting to fetch data #{i}")
result = fetch_with_timeout(i, timeout=3)
if result == "TIMEOUT":
print(f"Main thread: Fetch for data #{i} timed out, moving on...")
elif result is None:
print(f"Main thread: Fetch for data #{i} completed but returned no data.")
else:
print(f"Main thread: Successfully fetched: {result}")
print("\nAll fetch attempts completed!")
python3 /home/labex/project/thread_with_timeout.py
出力は異なりますが、次のようなものになるはずです。
Attempting to fetch data #1
Fetching data #1...
Data #1: Fetch completed in 1 seconds!
Main thread: Successfully fetched: Data content for #1
Attempting to fetch data #2
Fetching data #2...
Data #2: Fetch completed in 1 seconds!
Main thread: Successfully fetched: Data content for #2
Attempting to fetch data #3
Fetching data #3...
Data #3: Fetch timed out after 3 seconds!
Main thread: Fetch for data #3 timed out, moving on...
Data #3: Fetch took too long!
Attempting to fetch data #4
Fetching data #4...
Data #4: Fetch completed in 1 seconds!
Main thread: Successfully fetched: Data content for #4
Attempting to fetch data #5
Fetching data #5...
Data #5: Fetch completed in 1 seconds!
Main thread: Successfully fetched: Data content for #5
All fetch attempts completed!
この例は、以下を示しています。
Python では、デーモンスレッドはバックグラウンドで実行されるスレッドです。デーモン(daemon)スレッドと非デーモン(non-daemon)スレッドの主な違いは、Python が終了する前にデーモンスレッドの完了を待たないことです。これは、プログラムの終了を妨げないバックグラウンドタスクを実行するスレッドに役立ちます。
デーモンスレッドを示す例を作成しましょう。
/home/labex/project
ディレクトリに daemon_threads.py
という名前の新しいファイルを作成します。
次のコードを追加します。
import threading
import time
def background_task(name, interval):
"""A task that runs in the background at regular intervals."""
count = 0
while True:
count += 1
print(f"{name}: Iteration {count} at {time.strftime('%H:%M:%S')}")
time.sleep(interval)
def main_task():
"""The main task that runs for a set amount of time."""
print("Main task: Starting...")
time.sleep(5)
print("Main task: Completed!")
## Create two background threads
print("Creating background monitoring threads...")
monitor1 = threading.Thread(target=background_task, args=("Monitor-1", 1), daemon=True)
monitor2 = threading.Thread(target=background_task, args=("Monitor-2", 2), daemon=True)
## Start the background threads
monitor1.start()
monitor2.start()
print("Background monitors started, now starting main task...")
## Run the main task
main_task()
print("Main task completed, program will exit without waiting for daemon threads.")
print("Daemon threads will be terminated when the program exits.")
python3 /home/labex/project/daemon_threads.py
出力は次のようなものになるはずです。
Creating background monitoring threads...
Background monitors started, now starting main task...
Main task: Starting...
Monitor-1: Iteration 1 at 14:25:10
Monitor-2: Iteration 1 at 14:25:10
Monitor-1: Iteration 2 at 14:25:11
Monitor-1: Iteration 3 at 14:25:12
Monitor-2: Iteration 2 at 14:25:12
Monitor-1: Iteration 4 at 14:25:13
Monitor-1: Iteration 5 at 14:25:14
Monitor-2: Iteration 3 at 14:25:14
Main task: Completed!
Main task completed, program will exit without waiting for daemon threads.
Daemon threads will be terminated when the program exits.
この例では、次のことが行われました。
daemon=True
を設定し、それらをデーモンスレッドとしてマークしました。違いをよりよく理解するために、デーモンスレッドと非デーモンスレッドを比較するもう 1 つの例を作成しましょう。
/home/labex/project
ディレクトリに daemon_comparison.py
という名前の新しいファイルを作成します。
次のコードを追加します。
import threading
import time
def task(name, seconds, daemon=False):
"""A task that runs for a specified amount of time."""
print(f"{name} starting {'(daemon)' if daemon else '(non-daemon)'}")
time.sleep(seconds)
print(f"{name} finished after {seconds} seconds")
## Create a non-daemon thread that runs for 8 seconds
non_daemon_thread = threading.Thread(
target=task,
args=("Non-daemon thread", 8, False),
daemon=False ## This is the default, so it's not actually needed
)
## Create a daemon thread that runs for 8 seconds
daemon_thread = threading.Thread(
target=task,
args=("Daemon thread", 8, True),
daemon=True
)
## Start both threads
non_daemon_thread.start()
daemon_thread.start()
## Let the main thread run for 3 seconds
print("Main thread will run for 3 seconds...")
time.sleep(3)
## Check which threads are still running
print("\nAfter 3 seconds:")
print(f"Daemon thread is alive: {daemon_thread.is_alive()}")
print(f"Non-daemon thread is alive: {non_daemon_thread.is_alive()}")
print("\nMain thread is finishing. Here's what will happen:")
print("1. The program will wait for all non-daemon threads to complete")
print("2. Daemon threads will be terminated when the program exits")
print("\nWaiting for non-daemon threads to finish...")
## We don't need to join the non-daemon thread, Python will wait for it
## But we'll explicitly join it for clarity
non_daemon_thread.join()
print("All non-daemon threads have finished, program will exit now.")
python3 /home/labex/project/daemon_comparison.py
出力は次のようになります。
Non-daemon thread starting (non-daemon)
Daemon thread starting (daemon)
Main thread will run for 3 seconds...
After 3 seconds:
Daemon thread is alive: True
Non-daemon thread is alive: True
Main thread is finishing. Here's what will happen:
1. The program will wait for all non-daemon threads to complete
2. Daemon threads will be terminated when the program exits
Waiting for non-daemon threads to finish...
Non-daemon thread finished after 8 seconds
All non-daemon threads have finished, program will exit now.
主な観察事項:
デーモンスレッドは、以下に役立ちます。
非デーモンスレッドは、以下に適しています。
各タイプを使用するタイミングを理解することは、堅牢なマルチスレッドアプリケーションを設計する上で重要な部分です。
この実験(Lab)では、Python のスレッドを操作するための基本的なテクニックと、それらの完了を待つ方法について学びました。以下に、カバーされた主要な概念の概要を示します。
スレッドの作成と開始: スレッドオブジェクトを作成し、ターゲット関数を指定し、start()
メソッドでその実行を開始する方法を学びました。
join() を使用したスレッドの待機: メインプログラムを続行する前に、スレッドが完了するのを待つために join()
メソッドを使用する方法を発見し、適切な同期を確保しました。
複数のスレッドの操作: 複数のスレッドを手動で作成および管理し、より効率的なスレッド管理のために ThreadPoolExecutor
クラスを使用する練習をしました。
スレッドのタイムアウトとデーモンスレッド: スレッド操作のタイムアウトの設定や、バックグラウンドタスクにデーモンスレッドを使用するなど、高度なトピックを探求しました。
これらのスキルは、Python でマルチスレッドアプリケーションを開発するための基盤を提供します。マルチスレッド化により、プログラムは複数のタスクを同時に実行できるようになり、パフォーマンスと応答性が向上します。特に、I/O バウンド操作の場合に効果的です。
スレッドを使い続ける際には、次のベストプラクティスを覚えておいてください。
ThreadPoolExecutor
のようなより高レベルの抽象化を使用することを検討するこれらのスキルと実践により、マルチスレッド化技術を使用して、より効率的で応答性の高い Python アプリケーションを構築できるようになりました。