사용하기 쉬운 스레딩 (Threading)

PythonBeginner
지금 연습하기

소개

이 튜토리얼에서는 Python 의 threading 모듈을 사용하여 여러 실행 스레드를 동시에 실행하는 방법을 배우겠습니다.

Python 의 threading 모듈은 Python 프로그램에서 스레드를 생성하고 관리하는 간단한 방법을 제공합니다. 스레드는 프로그램 내에서 별도의 실행 흐름입니다. 여러 스레드를 동시에 실행함으로써, 멀티 코어 CPU 를 활용하고 프로그램의 성능을 향상시킬 수 있습니다.

threading 모듈은 스레드를 생성하고 관리하기 위한 두 가지 클래스를 제공합니다.

  1. Thread 클래스: 이 클래스는 단일 실행 스레드를 나타냅니다.
  2. Lock 클래스: 이 클래스는 스레드 간의 공유 리소스에 대한 접근을 동기화할 수 있도록 합니다.

스레드 생성 (Creating Threads)

Python 에서 새로운 스레드를 생성하려면, Thread 클래스의 새로운 인스턴스를 생성하고 실행할 함수를 전달해야 합니다.

WebIDE 에서 create_thread.py라는 프로젝트를 생성하고 다음 내용을 입력합니다.

import threading

## Define a function to run in the thread
def my_function():
    print("Hello from thread")

## Create a new thread
thread = threading.Thread(target=my_function)

## Start the thread
thread.start()

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

## Print "Done" to indicate that the program has finished
print("Done")

이 예제는 메시지를 출력하는 함수 my_function을 정의합니다. 그런 다음 Thread 클래스의 새로운 인스턴스를 생성하고, 대상 함수로 my_function을 전달합니다. 마지막으로, start 메서드를 사용하여 스레드를 시작하고, join 메서드를 사용하여 스레드가 완료될 때까지 기다립니다.

다음 명령을 사용하여 스크립트를 실행합니다.

python create_thread.py

동기화 (Synchronization)

여러 스레드가 동일한 공유 리소스 (예: 변수 또는 파일) 에 접근하는 경우, 경쟁 조건 (race condition) 을 방지하기 위해 해당 리소스에 대한 접근을 동기화해야 합니다. Python 의 threading 모듈은 이러한 목적으로 Lock 클래스를 제공합니다.

다음은 Lock을 사용하는 예시입니다. WebIDE 에서 sync.py라는 프로젝트를 생성하고 다음 내용을 입력합니다.

import threading

## Create a lock to protect access to shared resource
lock = threading.Lock()

## Shared resource that will be modified by multiple threads
counter = 0

## Define a function that each thread will run
def my_function():
    global counter

    ## Acquire the lock before accessing the shared resource
    lock.acquire()
    try:
        ## Access the shared resource
        counter += 1
    finally:
        ## Release the lock after modifying the shared resource
        lock.release()

## Create multiple threads to access the shared resource
threads = []

## Create and start 10 threads that execute the same function
for i in range(10):
    thread = threading.Thread(target=my_function)
    threads.append(thread)
    thread.start()

## Wait for all threads to complete their execution
for thread in threads:
    thread.join()

## Print the final value of the counter
print(counter) ## Output: 10

이 예제에서는 Lock 객체와 공유 리소스 counter를 생성합니다. my_function 함수는 acquire 메서드를 사용하여 락을 획득하고 release 메서드를 사용하여 락을 해제함으로써 공유 리소스에 접근합니다. 여러 스레드를 생성하고 시작한 다음, join 메서드를 사용하여 완료될 때까지 기다립니다. 마지막으로, counter 의 최종 값을 출력합니다.

다음 명령을 사용하여 스크립트를 실행합니다.

python sync.py

인자 있는 스레드 (Thread with Arguments)

Python 에서는 새로운 스레드를 생성할 때 args 매개변수를 사용하거나, Thread 클래스를 서브클래싱하고 인자를 받는 생성자를 정의하여 스레드에 인자를 전달할 수 있습니다. 다음은 두 가지 접근 방식의 예시입니다.

1; Thread 클래스의 서브클래스를 생성하고 run 메서드를 재정의하여 스레드의 동작을 정의합니다. WebIDE 에서 thread_subclass.py라는 프로젝트를 생성하고 다음 내용을 입력합니다.

import threading

## Define a custom Thread class that extends the Thread class
class MyThread(threading.Thread):
    ## Override the run() method to implement thread's behavior
    def run(self):
        print("Hello from thread")

## Create an instance of the custom thread class
thread = MyThread()

## Start the thread
thread.start()

## Wait for the thread to finish execution
thread.join()

다음 명령을 사용하여 스크립트를 실행합니다.

python thread_subclass.py

2; 스레드를 생성하고 args 매개변수를 사용하여 대상 함수에 인자를 전달합니다. WebIDE 에서 thread_with_args.py라는 프로젝트를 생성하고 다음 내용을 입력합니다.

import threading

## Define a function that takes a parameter
def my_function(name):
    print("Hello from", name)

## Create a new thread with target function and arguments
thread = threading.Thread(target=my_function, args=("Thread 1",))

## Start the thread
thread.start()

## Wait for the thread to finish execution
thread.join()

다음 명령을 사용하여 스크립트를 실행합니다.

python thread_with_args.py

스레드 풀 (Thread Pool)

Python 에서는 미리 정의된 스레드 집합을 사용하여 작업을 동시에 실행하기 위해 스레드 풀을 사용할 수 있습니다. 스레드 풀을 사용하면 각 작업에 대해 스레드를 생성하고 소멸하는 오버헤드를 피할 수 있어 성능을 향상시킬 수 있다는 장점이 있습니다.

Python 의 concurrent.futures 모듈은 스레드 풀을 생성하고 작업을 제출할 수 있는 ThreadPoolExecutor 클래스를 제공합니다. 다음은 예시입니다.

WebIDE 에서 thread_pool_range.py라는 프로젝트를 생성하고 다음 내용을 입력합니다.

import concurrent.futures

## Define a function to be executed in multiple threads with two arguments
def my_func(arg1, arg2):
    ## Define the tasks performed by the thread here
    print(f"Hello from my thread with args {arg1} and {arg2}")

## Create a ThreadPoolExecutor object with a maximum of 5 worker threads
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    ## Submit each task (function call with its arguments) to the executor for processing in a separate thread
    ## The submit() method returns a Future object representing the result of the asynchronous computation
    for i in range(10):
        executor.submit(my_func, i, i+1)

이 예제에서는 두 개의 인수를 받는 my_func 함수를 정의합니다. 최대 5 개의 작업자 스레드를 가진 ThreadPoolExecutor를 생성합니다. 그런 다음, 숫자 범위를 반복하면서 executor.submit() 메서드를 사용하여 스레드 풀에 작업을 제출합니다. 제출된 각 작업은 사용 가능한 작업자 스레드 중 하나에서 실행됩니다.

팁: ThreadPoolExecutor 객체는 컨텍스트 관리자로 사용됩니다. 이렇게 하면 with 블록 내부의 코드가 완료될 때 모든 스레드가 제대로 정리됩니다.

다음 명령을 사용하여 스크립트를 실행합니다.

python thread_pool_range.py

submit() 메서드는 제출된 작업의 결과를 나타내는 Future 객체를 즉시 반환합니다. Future 객체의 result() 메서드를 사용하여 작업의 반환 값을 검색할 수 있습니다. 작업에서 예외가 발생하면 result()를 호출하면 해당 예외가 발생합니다.

또한 ThreadPoolExecutor 클래스의 map() 메서드를 사용하여 동일한 함수를 항목 모음에 적용할 수 있습니다. 예를 들어, WebIDE 에서 thread_pool_map.py라는 프로젝트를 생성하고 다음 내용을 입력합니다.

import concurrent.futures

## Define a function to be executed in multiple threads
def my_func(item):
    ## Define the tasks performed by the thread here
    print(f"Hello from my thread with arg {item}")

## Create a list of items to be processed by the threads
items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

## Create a ThreadPoolExecutor object with a maximum of 5 worker threads
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    ## Submit each item to the executor for processing in a separate thread
    ## The map() method automatically returns the results in order
    executor.map(my_func, items)

이 예제에서는 하나의 인수를 받는 my_func 함수를 정의합니다. 항목 목록을 생성하고 executor.map() 메서드를 사용하여 스레드 풀에 제출합니다. 목록의 각 항목은 my_func에 인수로 전달되며, 각 항목은 사용 가능한 작업자 스레드 중 하나에서 실행됩니다.

다음 명령을 사용하여 스크립트를 실행합니다.

python thread_pool_map.py

thread_pool_range.pythread_pool_map.py에서 얻은 결과는 동일합니다.

데몬 스레드 (Daemon Threads)

Python 에서 데몬 스레드는 백그라운드에서 실행되며 프로그램이 종료되는 것을 막지 않는 유형의 스레드입니다. 모든 비 - 데몬 스레드가 완료되면, Python 인터프리터는 데몬 스레드가 아직 실행 중인지 여부에 관계없이 종료됩니다.

WebIDE 에서 daemon_thread_with_args.py라는 프로젝트를 생성하고 다음 내용을 입력합니다.

import threading
import time

## Define a function that runs indefinitely and prints messages at a regular interval
def my_function():
    while True:
        print("Hello from thread")
        time.sleep(1)

## Create a daemon thread that runs the target function
thread = threading.Thread(target=my_function, daemon=True)

## Start the thread
thread.start()

## The main program continues to execute and prints a message
print("Main program")

## Wait for a few seconds before exiting the program
time.sleep(5)

## The program exits, and the daemon thread is terminated automatically
print("Main thread exiting...")

이 예제에서는 무한 루프를 실행하고 time.sleep() 함수를 사용하여 매 초 메시지를 출력하는 스레드를 생성합니다. 메인 프로그램이 자동으로 종료될 때 종료되도록 daemon 매개변수를 사용하여 스레드를 데몬으로 표시합니다. 메인 프로그램은 계속 실행되고 메시지를 출력합니다. 몇 초 동안 기다린 후 프로그램이 종료되고 데몬 스레드가 종료됩니다.

그런 다음 다음 명령을 사용하여 스크립트를 실행합니다.

python daemon_thread_with_args.py

물론, 스레드 인스턴스에서 setDaemon(True) 메서드를 호출하여 스레드를 데몬으로 설정할 수도 있습니다. 예를 들어, WebIDE 에서 daemon_thread_with_func.py라는 프로젝트를 생성하고 다음 내용을 입력합니다.

import threading
import time

## Define a function that runs indefinitely and prints messages at a regular interval
def my_function():
    while True:
        print("Hello from thread")
        time.sleep(1)

## Create a new thread with target function
thread = threading.Thread(target=my_function)

## Set the daemon flag to True so that the thread runs in the background and terminates when the main program exits
thread.setDaemon(True)

## Start the thread
thread.start()

## The main program continues to run and prints a message
print("Main program")

## Wait for a few seconds before exiting the program
time.sleep(5)

## The program exits and the daemon thread is terminated automatically
print("Main thread exiting...")

다음 명령으로 스크립트를 실행하면 위의 예제와 동일한 결과를 얻을 수 있습니다.

python daemon_thread_with_func.py

이벤트 객체 (Event Object)

Python 에서는 threading.Event 객체를 사용하여 스레드가 특정 이벤트가 발생할 때까지 기다린 후 진행하도록 할 수 있습니다. Event 객체는 한 스레드가 이벤트가 발생했음을 알리고 다른 스레드가 해당 신호를 기다릴 수 있는 방법을 제공합니다.

WebIDE 에서 event_object.py라는 프로젝트를 생성하고 다음 내용을 입력합니다.

import threading

## Create an event object
event = threading.Event()

## Define a function that waits for the event to be set
def my_function():
    print("Waiting for event")
    ## Wait for the event to be set
    event.wait()
    print("Event received")

## Create a new thread with target function
thread = threading.Thread(target=my_function)

## Start the thread
thread.start()

## Signal the event after a few seconds
## The wait() call in the target function will now return and continue execution
event.set()

## Wait for the thread to finish executing
thread.join()

이 예제에서는 Event 클래스를 사용하여 Event 객체를 생성합니다. wait 메서드를 사용하여 이벤트가 신호를 받을 때까지 기다린 다음 메시지를 출력하는 함수를 정의합니다. 새 스레드를 생성하고 시작합니다. 몇 초 후에 set 메서드를 사용하여 이벤트를 알립니다. 스레드는 이벤트를 수신하고 메시지를 출력합니다. 마지막으로, join 메서드를 사용하여 스레드가 완료될 때까지 기다립니다.

그런 다음 다음 명령을 사용하여 스크립트를 실행합니다.

python event_object.py

타이머 객체 (Timer Object)

Python 에서는 threading.Timer 객체를 사용하여 특정 시간이 경과한 후 실행할 함수를 예약할 수 있습니다. Timer 객체는 함수를 실행하기 전에 지정된 시간 간격을 기다리는 새 스레드를 생성합니다.

WebIDE 에서 timer_object.py라는 프로젝트를 생성하고 다음 내용을 입력합니다.

import threading

## Define a function to be executed by the Timer after 5 seconds
def my_function():
    print("Hello from timer")

## Create a timer that runs the target function after 5 seconds
timer = threading.Timer(5, my_function)

## Start the timer
timer.start()

## Wait for the timer to finish
timer.join()

이 예제에서는 Timer 클래스를 사용하여 Timer 객체를 생성하고, 초 단위의 시간 지연과 실행할 함수를 전달합니다. start 메서드를 사용하여 타이머를 시작하고, join 메서드를 사용하여 완료될 때까지 기다립니다. 5 초 후에 함수가 실행되고 메시지를 출력합니다.

그런 다음 다음 명령을 사용하여 스크립트를 실행합니다.

python timer_object.py

팁: 타이머 스레드는 별도로 실행되므로 메인 스레드와 동기화되지 않을 수 있습니다. 함수가 일부 공유 상태 또는 리소스에 의존하는 경우, 적절하게 해당 리소스에 대한 접근을 동기화해야 합니다. 또한, 다른 모든 비 - 데몬 스레드가 완료된 경우에도 타이머 스레드가 여전히 실행 중이면 프로그램이 종료되는 것을 막지 않는다는 점을 기억하십시오.

배리어 객체 (Barrier Object)

Python 에서는 threading.Barrier 객체를 사용하여 미리 정의된 동기화 지점에서 여러 스레드를 동기화할 수 있습니다. Barrier 객체는 일련의 스레드가 실행을 계속하기 전에 서로 특정 지점에 도달할 때까지 기다릴 수 있는 방법을 제공합니다.

WebIDE 에서 barrier_object.py라는 프로젝트를 생성하고 다음 내용을 입력합니다.

import threading

## Create a Barrier object for 3 threads
barrier = threading.Barrier(3)

## Define a function that waits at the barrier
def my_function():
    print("Before barrier")
    ## Wait for all three threads to reach the barrier
    barrier.wait()
    print("After barrier")

## Create 3 threads using a loop and start them
threads = []
for i in range(3):
    thread = threading.Thread(target=my_function)
    threads.append(thread)
    thread.start()

## Wait for all threads to finish executing
for thread in threads:
    thread.join()

이 예제에서는 Barrier 클래스를 사용하여 Barrier 객체를 생성하고, 기다릴 스레드 수를 전달합니다. wait 메서드를 사용하여 배리어를 기다리고 메시지를 출력하는 함수를 정의합니다. 세 개의 스레드를 생성하고 시작합니다. 각 스레드는 배리어를 기다리므로 모든 스레드가 모두 배리어에 도달할 때까지 기다립니다. 마지막으로, join 메서드를 사용하여 모든 스레드가 완료될 때까지 기다립니다.

그런 다음 다음 명령을 사용하여 스크립트를 실행합니다.

python barrier_object.py

요약

이것으로 끝입니다! 이제 Python threading 모듈을 코드에서 사용하는 방법을 알게 되었습니다. 이는 효율적인 동시 애플리케이션을 더 잘 개발할 수 있도록 동시 프로그래밍의 기본 원리와 기술을 깊이 있게 숙달하는 데 도움이 될 수 있습니다.