파이썬 쓰레드(Thread)Python Threads
쓰레드의 기본 개념부터 파이썬에서의 쓰레드 구현 방법From the basic concept of threads to implementing them in Python
소프트웨어는 여러 작업을 동시에 처리해야 하는 경우가 많습니다. 이를 위해 프로그램, 프로세스, 쓰레드의 개념을 이해하는 것이 중요합니다.
프로그램(Program)
일반적으로 프로그램이란 하드 디스크(HDD), SSD 같은 디스크에 저장된 정적인 상태의 명령어 집합을 의미합니다. ‘정적인 상태’란 말은, 실행 중이지 않은 상태의 코드를 의미 합니다.
%%{
init: {
'theme': 'base',
'themeVariables': {
'lineColor': '#2196f3',
'primaryTextColor': '#fff',
'tertiaryColor': '#333'
}
}
}%%
flowchart LR
subgraph "Hard Disk"
Program[Program]
end
그렇다면 ‘실행 중’이라는 상태는 어떤 것을 의미 할까요? 프로그램이 실행되면 어떻게 되는지 알아봅시다.
프로세스(Process)
프로그램이 실행되면 독립된 메모리 공간을 할당받고 CPU가 명령어를 처리 할 수 있는 상태가 됩니다. 그리고 이런 상태를 ‘프로세스(Process)’라 합니다.
%%{
init: {
'theme': 'base',
'themeVariables': {
'lineColor': '#2196f3',
'primaryTextColor': '#fff',
'tertiaryColor': '#333'
}
}
}%%
flowchart LR
subgraph "Hard Disk"
Program[Program]
end
subgraph "Memory"
Process[Process]
end
Program --- Process
Chrome 브라우저
Chrome 브라우저는 하나의 탭이 하나의 프로세스로 동작하는 멀티 프로세스(Multi Process) 방식으로 동작합니다. 각 탭은 독립적으로 동작하기 때문에 하나의 탭에 오류가 발생해도 다른 탭은 안정적으로 사용 할 수 있습니다.

> 탭 하나가 말썽이지만 크게 상관없다
프로세스 특징
프로세스의 메모리 구성
각 프로세스는 아래와 같이 독립된 메모리 공간을 할당 받습니다. 그래서 한 프로세스가 비정상 종료되어도 다른 프로세스에 영향을 주지 않고 안정적으로 동작 할 수 있습니다.
%%{
init: {
'theme': 'base',
'themeVariables': {
'lineColor': '#2196f3',
'primaryTextColor': '#fff',
'tertiaryColor': '#333'
}
}
}%%
flowchart LR
subgraph "Hard Disk"
Program[Chrome.exe]
end
subgraph "Memory"
Process1[Chrome Tab A]
Process2[Chrome Tab B]
end
Program --- Process1
Program --- Process2
프로세스는 메모리를 크게 4가지의 영역으로 나누어 사용합니다.
%%{
init: {
'theme': 'base',
'themeVariables': {
'lineColor': '#2196f3',
'primaryTextColor': '#000',
'tertiaryColor': '#333'
}
}
}%%
flowchart TB
subgraph "Memory"
subgraph "Process"
CS1[Code Section]
DS1[Data Section]
HS1[Heap Section]
SS1[Stack Section]
end
end
style Process fill:#555,stroke:#333,stroke-width:1px
style CS1 fill:#bbf,stroke:#333,stroke-width:1px
style DS1 fill:#ddf,stroke:#333,stroke-width:1px
style HS1 fill:#ffd,stroke:#333,stroke-width:1px
style SS1 fill:#dfd,stroke:#333,stroke-width:1px
- 코드 영역(Code Section) : 실행 코드
- 데이터 영역(Data Section) : 전역 변수/정적 변수
- 힙 영역(Heap Section) : 동적 메모리 할당 영역
- 스택 영역(Stack Section) : 지역 변수, 함수 호출 정보
IPC(Inter-Process Communication)
프로세스는 독립적으로 동작하기 때문에 프로세스끼리 통신하기 위한 다양한 방식을 IPC(Inter-Process Communication)라 합니다. 파이프(Pipe), 소켓(Socket), 공유메모리(Shared Memory), 메시지 큐(Message Queue) 등의 방식을 사용합니다. 이런 통신 과정에서 약간의 오버헤드가 발생 할 수 있습니다.
PCB(Process Control Block)
프로세스 내부에 PCB(Process Control Block)라는 데이터 구조를 생성 합니다. 이 PCB에는 프로세스에 대한 다양한 정보가 저장됩니다.
- 프로세스 ID
- 프로세스 상태
- 프로그램 카운터
- CPU 레지스터 등
그리고 이 정보를 기반으로 각 프로세스마다 CPU, 메모리 등 시스템 자원을 할당받아 사용합니다.
쓰레드(Thread)
프로세스는 메모리를 할당 받아 명령어를 실행할 수 있는 상태를 뜻했습니다. 그렇다면 프로세스에서 누가 명령어를 실행하는 걸까요? 바로 쓰레드(Thread)가 그 역할을 수행합니다.
thread
- 실
- (이야기 등의) 가닥[맥락]
- (실 등을) 꿰다
쓰레드는 프로세스 내에서 실행되는 작은 실행 단위를 의미합니다. 단어의 뜻처럼 “실행의 흐름” 또는 “제어의 흐름”라고 생각하면 됩니다. 그리고 프로세스는 최소 1개 이상의 쓰레드를 포함합니다.
%%{
init: {
'theme': 'base',
'themeVariables': {
'lineColor': '#2196f3',
'primaryTextColor': '#000',
'tertiaryColor': '#333'
}
}
}%%
flowchart BT
subgraph "Memory"
subgraph "Process"
T11[Thread 1]
CODE1[code]
CODE2[code]
end
end
T11 --- CODE1
CODE1 --- CODE2
style Process fill:#555,stroke:#333,stroke-width:1px
style T11 fill:#fff,stroke:#333,stroke-width:1px
style CODE1 fill:#bbf,stroke:#333,stroke-width:1px
style CODE2 fill:#bbf,stroke:#333,stroke-width:1px
쓰레드의 자원 공유
하나의 프로세스 내에 두 개의 쓰레드가 있다고 가정해 봅시다. 각 쓰레드는 스택만 따로 할당받고 나머지 메모리 공간은 프로세스 내에서 공유하게 됩니다.
flowchart TB
subgraph CPU["CPU"]
Registers["레지스터"]
end
subgraph Process["Process"]
subgraph Shared["공유 메모리 영역"]
direction TB
Code["Code 영역
(프로그램 코드)"]
Data["Data 영역
(전역/Static 변수)"]
Heap["Heap 영역
(동적 할당 메모리)"]
end
subgraph ThreadStacks["쓰레드별 스택"]
Stack1["Thread 1 Stack"]
Stack2["Thread 2 Stack"]
end
end
%% 스타일 정의
style Process fill:#555,stroke:#333,stroke-width:1px,color:#bbb
classDef cpu fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px
classDef shared fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
classDef thread fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
classDef process fill:#f5f5f5,stroke:#333,stroke-width:2px
class CPU cpu
class Shared shared
class ThreadContexts,ThreadStacks thread
class Process process
명령을 처리하는 CPU 코어 하나는 한 시점에 하나의 쓰레드만 실행 가능합니다. 때문에 여러 쓰레드가 동시에 실행하는 것처럼 보여도 사실은 여러 쓰레드를 번갈아가면서 빠르게 동작합니다.
컨텍스트 스위칭(context switching)
컨텍스트 스위칭(context switching)은 현재 실행 중인 프로세스/쓰레드를 중단하고 다른 프로세스/쓰레드를 실행하는 것을 뜻합니다. 이때마다 이전의 상태(컨텍스트)를 보관하고 새로운 상태로 전환하는 작업을 수행합니다.

> 출처 : Akka in Action
쓰레드가 많아지면 처리도 빨라질까요? 정답일 수도 있고 아닐수도 있습니다. 왜냐하면 컨텍스트 스위칭도 하나의 작업이기 때문에, 너무 자주 발생하면 오버헤드가 됩니다. 따라서 적절한 균형을 맞추는 것이 중요합니다.
파이썬 쓰레드
파이썬에서는 기본적으로 하나의 프로세스에 하나의 쓰레드가 생성됩니다. 그리고 이 쓰레드를 메인 쓰레드(MainThread)라고 합니다. 메인 쓰레드에서 다른 쓰레드를 생성하고 사용하는 방법을 알아 보겠습니다.
%%{
init: {
'theme': 'base',
'themeVariables': {
'lineColor': '#2196f3',
'primaryTextColor': '#000',
'tertiaryColor': '#333'
}
}
}%%
flowchart BT
subgraph "Memory"
subgraph "Process"
T11[Main Thread]
T12[Sub Thread]
CODE1[code]
CODE2[code]
end
end
T11 --- CODE1
T12 --- CODE2
style Process fill:#555,stroke:#333,stroke-width:1px
style T11 fill:#fff,stroke:#333,stroke-width:1px
style T12 fill:#fff,stroke:#333,stroke-width:1px
style CODE1 fill:#bbf,stroke:#333,stroke-width:1px
style CODE2 fill:#bbf,stroke:#333,stroke-width:1px
threading 모듈
파이썬에서는 threading 내장 모듈에 있는 Thread 클래스를 통해 쓰레드를 만들 수 있습니다. start 메서드로 쓰레드를 실행 할 수 있습니다.
-
target: 쓰레드로 실행할 함수 -
args: iterable 인자값 전달 -
kwargs: dict 형태 인자값 전달
import threading
thread = threading.Thread(target=lambda x: print(f"Hello, {x}"), args=("Thread",))
thread.start()
Hello, Thread
threading.Thread 클래스 상속
Thread 클래스를 상속하여 쓰레드 클래스를 정의 할 수 있습니다. target 인자값을 사용하는 대신, run 메서드를 오버라이드하여 쓰레드가 실행할 작업 정의합니다.
import threading
class MyThread(threading.Thread):
def __init__(self, name: str):
super().__init__()
self.name = name
def run(self):
print(f"Hello, {self.name}")
if __name__ == "__main__":
thread = MyThread("MyThread")
thread.start()
현재 쓰레드 정보
current_thread 메서드를 통해 현재 쓰레드의 정보를 확인 할 수 있습니다.
-
ident: 쓰레드 고유 식별자 -
name:- 기본적으로 “Thread-숫자 (함수 명)”로 표현
- 메인 쓰레드의 경우 = MainThread
import threading as th
def sub_thread():
print(f"{th.current_thread().name} : {th.current_thread().ident}")
thread1 = th.Thread(target=sub_thread)
thread2 = th.Thread(target=sub_thread)
if __name__ == "__main__":
thread1.start()
thread2.start()
print(f"{th.current_thread().name} Done")
Thread-1 (sub_thread) : 139696827188800
Thread-2 (sub_thread) : 140045886248512
MainThread Done
쓰레드의 동시성(Concurrency)
쓰레드는 서로 번갈아가면서 실행되는 동시성(Concurrency) 특징이 있습니다. 이는 실제로 동시에 실행되는 것이 아니라, CPU가 빠르게 쓰레드를 전환하면서 실행하기 때문입니다.
아래의 코드를 실행하면 메인 쓰레드와 서브 쓰레드의 실행 순서가 매번 달라질 수 있으며, 이는 운영체제의 스케줄링에 따라 결정됩니다.
import threading as th
def print_numbers(i: int):
for j in range(1, i + 1):
print(f"Sub Thread : {j}")
thread1 = th.Thread(target=print_numbers, args=(100,))
if __name__ == "__main__":
thread1.start()
print("MainThread Done")
Sub Thread : 6
Sub Thread : 7
...
MainThread Done
...
Sub Thread: 100
join 메서드
쓰레드가 다른 쓰레드의 실행이 끝날때 까지 대기하는 join 메서드에 대해서 알아봅시다. 작업의 순서를 제어하거나 의존성 있는 작업을 처리할 때 유용하게 사용할 수 있습니다.
import threading as th
def print_numbers(i: int):
for j in range(1, i + 1):
print(f"Sub Thread : {j}")
sub_thread = th.Thread(target=print_numbers, args=(100,))
if __name__ == "__main__":
sub_thread.start()
sub_thread.join()
print("MainThread Done")
join 메서드를 사용하면 메인 쓰레드(MainThread)에서 서브 쓰레드(Sub Thread)의 작업이 끝날때까지 기다리고 Done 메시지가 출력되는 것을 볼 수 있습니다.
...
Sub Thread : 99
Sub Thread : 100
MainThread Done
여러개의 쓰레드를 순서적으로 실행하고 싶은 경우는 다음과 같이 사용 할 수 있습니다. thread1의 작업이 모두 끝난 뒤에 thread2의 작업이 시작 되는 것을 확인 할 수 있습니다.
if __name__ == "__main__":
thread1.start()
thread1.join()
thread2.start()
thread2.join()
print("MainThread Done")
데몬(daemon) 쓰레드
데몬(daemon) 쓰레드는 메인 쓰레드가 종료되면 함께 종료되는 백그라운드 쓰레드입니다. 주로 메인 쓰레드의 보조 역할을 하는 작업에 사용됩니다. 기존 쓰레드를 daemon 인자값으로 설정하여 만들수 있습니다.
daemon_thread = th.Thread(target=background_task, daemon=True)
import threading as th
import time
def background_task():
while True:
print("Daemon thread is running...")
time.sleep(1)
daemon_thread = th.Thread(target=background_task, daemon=True)
if __name__ == "__main__":
daemon_thread.start()
print("Main thread is running...")
time.sleep(3)
print("Main thread is ending...")
Main thread is running...
Daemon thread is running...
Daemon thread is running...
Daemon thread is running...
Main thread is ending...
Daemon thread is running...
is_alive 메서드
is_alive 메서드를 사용하여 스레드가 현재 실행 중인지 확인 할 수 있습니다. 하나의 쓰레드에서 다른 쓰레드가 동작을 모니터링하는데 유용합니다. 특히 디버깅이나 로깅 시에 유용하게 활용할 수 있습니다.
예시 : 모니터링
import threading as th
import time
def monitor_thread(thread_to_monitor: th.Thread):
while thread_to_monitor.is_alive():
print(f"모니터링: {thread_to_monitor.name} 실행 중")
time.sleep(0.5)
print(f"모니터링: {thread_to_monitor.name} 종료됨")
def worker():
print(f"{th.current_thread().name} 시작")
time.sleep(1)
print(f"{th.current_thread().name} 종료")
if __name__ == "__main__":
work_thread = th.Thread(target=worker, name="WorkerThread")
monitor = th.Thread(
target=monitor_thread, args=(work_thread,), name="MonitorThread", daemon=True
)
work_thread.start()
monitor.start()
work_thread.join()
- work_thread를 먼저 생성하여 monitor 쓰레드에 전달합니다.
- monitor에서
is_alive()를 통해 work_thread가 동작 중인지 확인 합니다. - work_thread가 종료되면 메인 쓰레드(MainThread)가 종료 되면서 데몬 쓰레드인 monitor도 종료 됩니다.
WorkerThread 시작
모니터링: WorkerThread 실행 중
모니터링: WorkerThread 실행 중
WorkerThread 종료
모니터링: WorkerThread 종료됨
마무리
지금까지 우리는 다음과 같은 중요한 개념들을 살펴보았습니다.
- 프로그램이 실행 파일에서 메모리에 로드되어 프로세스가 되는 과정
- 프로세스 내에서 실제 작업을 수행하는 실행 단위인 쓰레드의 개념
- 파이썬에서 threading 모듈을 사용한 쓰레드 생성과 관리 방법
-
join(),is_alive(),daemon등 실용적인 쓰레드 제어 기법
더 해보기
더 효율적이고 안전한 멀티쓰레드 프로그래밍을 위해서 아래의 심화 주제들을 학습해보면 좋을듯 합니다. 😊
- 뮤텍스(Mutex)
- 세마포어(Semaphore)
- ThreadPoolExecutor를 활용한 효율적인 작업 처리
Software often has to handle several tasks at the same time. To do that, it’s important to understand the concepts of programs, processes, and threads.
Program
Generally, a program is a set of instructions stored in a static state on a disk such as a hard disk drive (HDD) or SSD. By ‘static state,’ I mean code that isn’t running.
%%{
init: {
'theme': 'base',
'themeVariables': {
'lineColor': '#2196f3',
'primaryTextColor': '#fff',
'tertiaryColor': '#333'
}
}
}%%
flowchart LR
subgraph "Hard Disk"
Program[Program]
end
So what does the ‘running’ state mean? Let’s look at what happens when a program runs.
Process
When a program runs, it’s given its own separate memory space and becomes ready for the CPU to process its instructions. This state is called a ‘process.’
%%{
init: {
'theme': 'base',
'themeVariables': {
'lineColor': '#2196f3',
'primaryTextColor': '#fff',
'tertiaryColor': '#333'
}
}
}%%
flowchart LR
subgraph "Hard Disk"
Program[Program]
end
subgraph "Memory"
Process[Process]
end
Program --- Process
The Chrome Browser
The Chrome browser uses a multi-process model, where each tab runs as its own process. Because each tab runs independently, even if one tab hits an error, the other tabs keep working reliably.

> One tab is acting up, but it's no big deal
Characteristics of a Process
Process Memory Layout
Each process is given its own separate memory space, as shown below. So even if one process terminates abnormally, it doesn’t affect the others, and they keep running reliably.
%%{
init: {
'theme': 'base',
'themeVariables': {
'lineColor': '#2196f3',
'primaryTextColor': '#fff',
'tertiaryColor': '#333'
}
}
}%%
flowchart LR
subgraph "Hard Disk"
Program[Chrome.exe]
end
subgraph "Memory"
Process1[Chrome Tab A]
Process2[Chrome Tab B]
end
Program --- Process1
Program --- Process2
A process divides its memory into four main sections.
%%{
init: {
'theme': 'base',
'themeVariables': {
'lineColor': '#2196f3',
'primaryTextColor': '#000',
'tertiaryColor': '#333'
}
}
}%%
flowchart TB
subgraph "Memory"
subgraph "Process"
CS1[Code Section]
DS1[Data Section]
HS1[Heap Section]
SS1[Stack Section]
end
end
style Process fill:#555,stroke:#333,stroke-width:1px
style CS1 fill:#bbf,stroke:#333,stroke-width:1px
style DS1 fill:#ddf,stroke:#333,stroke-width:1px
style HS1 fill:#ffd,stroke:#333,stroke-width:1px
style SS1 fill:#dfd,stroke:#333,stroke-width:1px
- Code Section : The executable code
- Data Section : Global and static variables
- Heap Section : Dynamically allocated memory
- Stack Section : Local variables and function call information
IPC (Inter-Process Communication)
Because processes run independently, the various ways they communicate with each other are called IPC (Inter-Process Communication). Methods include pipes, sockets, shared memory, and message queues. This communication can introduce some overhead.
PCB (Process Control Block)
A data structure called the PCB (Process Control Block) is created for each process. The PCB stores various information about the process.
- Process ID
- Process state
- Program counter
- CPU registers, etc.
Based on this information, each process is allocated system resources such as CPU time and memory.
Thread
A process is the state in which a program has been given memory and can execute instructions. So who actually executes the instructions in a process? That’s the job of a thread.
thread
- a thin strand of yarn
- a line (of a story or argument)
- to thread (a needle, etc.)
A thread is a small unit of execution that runs inside a process. As the word suggests, you can think of it as a “thread of execution” or a “flow of control.” Every process contains at least one thread.
%%{
init: {
'theme': 'base',
'themeVariables': {
'lineColor': '#2196f3',
'primaryTextColor': '#000',
'tertiaryColor': '#333'
}
}
}%%
flowchart BT
subgraph "Memory"
subgraph "Process"
T11[Thread 1]
CODE1[code]
CODE2[code]
end
end
T11 --- CODE1
CODE1 --- CODE2
style Process fill:#555,stroke:#333,stroke-width:1px
style T11 fill:#fff,stroke:#333,stroke-width:1px
style CODE1 fill:#bbf,stroke:#333,stroke-width:1px
style CODE2 fill:#bbf,stroke:#333,stroke-width:1px
How Threads Share Resources
Suppose there are two threads in a single process. Each thread gets its own stack, and the rest of the memory space is shared within the process.
flowchart TB
subgraph CPU["CPU"]
Registers["Registers"]
end
subgraph Process["Process"]
subgraph Shared["Shared memory"]
direction TB
Code["Code section
(program code)"]
Data["Data section
(global/static variables)"]
Heap["Heap section
(dynamically allocated memory)"]
end
subgraph ThreadStacks["Per-thread stacks"]
Stack1["Thread 1 Stack"]
Stack2["Thread 2 Stack"]
end
end
%% Style definitions
style Process fill:#555,stroke:#333,stroke-width:1px,color:#bbb
classDef cpu fill:#ffcdd2,stroke:#d32f2f,stroke-width:2px
classDef shared fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
classDef thread fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
classDef process fill:#f5f5f5,stroke:#333,stroke-width:2px
class CPU cpu
class Shared shared
class ThreadContexts,ThreadStacks thread
class Process process
A single CPU core can execute only one thread at any given moment. So even when several threads seem to run at the same time, they’re actually taking turns very quickly.
Context Switching
Context switching means pausing the currently running process/thread and running a different one. Each time, the previous state (the context) is saved and the switch to the new state is made.

> Source : Akka in Action
Does adding more threads make things faster? Maybe, maybe not. Context switching is itself a piece of work, so if it happens too often, it becomes overhead. That’s why striking the right balance matters.
Python Threads
In Python, a process gets one thread by default. This thread is called the main thread (MainThread). Let’s look at how to create and use other threads from the main thread.
%%{
init: {
'theme': 'base',
'themeVariables': {
'lineColor': '#2196f3',
'primaryTextColor': '#000',
'tertiaryColor': '#333'
}
}
}%%
flowchart BT
subgraph "Memory"
subgraph "Process"
T11[Main Thread]
T12[Sub Thread]
CODE1[code]
CODE2[code]
end
end
T11 --- CODE1
T12 --- CODE2
style Process fill:#555,stroke:#333,stroke-width:1px
style T11 fill:#fff,stroke:#333,stroke-width:1px
style T12 fill:#fff,stroke:#333,stroke-width:1px
style CODE1 fill:#bbf,stroke:#333,stroke-width:1px
style CODE2 fill:#bbf,stroke:#333,stroke-width:1px
The threading Module
In Python, you can create threads with the Thread class in the built-in threading module. You start a thread with the start method.
-
target: The function the thread will run -
args: Arguments passed as an iterable -
kwargs: Arguments passed as a dict
import threading
thread = threading.Thread(target=lambda x: print(f"Hello, {x}"), args=("Thread",))
thread.start()
Hello, Thread
Subclassing threading.Thread
You can define a thread class by subclassing Thread. Instead of using the target argument, you override the run method to define the work the thread will do.
import threading
class MyThread(threading.Thread):
def __init__(self, name: str):
super().__init__()
self.name = name
def run(self):
print(f"Hello, {self.name}")
if __name__ == "__main__":
thread = MyThread("MyThread")
thread.start()
Current Thread Information
You can check information about the current thread with the current_thread method.
-
ident: The thread’s unique identifier -
name:- By default, shown as “Thread-number (function name)”
- For the main thread = MainThread
import threading as th
def sub_thread():
print(f"{th.current_thread().name} : {th.current_thread().ident}")
thread1 = th.Thread(target=sub_thread)
thread2 = th.Thread(target=sub_thread)
if __name__ == "__main__":
thread1.start()
thread2.start()
print(f"{th.current_thread().name} Done")
Thread-1 (sub_thread) : 139696827188800
Thread-2 (sub_thread) : 140045886248512
MainThread Done
Thread Concurrency
Threads have the property of concurrency: they take turns running. They don’t actually run at the same time; the CPU switches between threads so quickly that it looks that way.
If you run the code below, the order in which the main thread and the sub thread run may differ each time, and that’s determined by the operating system’s scheduler.
import threading as th
def print_numbers(i: int):
for j in range(1, i + 1):
print(f"Sub Thread : {j}")
thread1 = th.Thread(target=print_numbers, args=(100,))
if __name__ == "__main__":
thread1.start()
print("MainThread Done")
Sub Thread : 6
Sub Thread : 7
...
MainThread Done
...
Sub Thread: 100
The join Method
Let’s look at the join method, which makes a thread wait until another thread finishes. It’s handy for controlling the order of work or handling tasks that depend on each other.
import threading as th
def print_numbers(i: int):
for j in range(1, i + 1):
print(f"Sub Thread : {j}")
sub_thread = th.Thread(target=print_numbers, args=(100,))
if __name__ == "__main__":
sub_thread.start()
sub_thread.join()
print("MainThread Done")
With the join method, you can see that the main thread (MainThread) waits until the sub thread (Sub Thread) finishes its work, and only then prints the Done message.
...
Sub Thread : 99
Sub Thread : 100
MainThread Done
If you want to run several threads one after another, you can do it like this. You’ll see that thread2 starts its work only after thread1 has completely finished.
if __name__ == "__main__":
thread1.start()
thread1.join()
thread2.start()
thread2.join()
print("MainThread Done")
Daemon Threads
A daemon thread is a background thread that ends when the main thread ends. It’s mainly used for work that supports the main thread. You create one by setting the daemon argument on a regular thread.
daemon_thread = th.Thread(target=background_task, daemon=True)
import threading as th
import time
def background_task():
while True:
print("Daemon thread is running...")
time.sleep(1)
daemon_thread = th.Thread(target=background_task, daemon=True)
if __name__ == "__main__":
daemon_thread.start()
print("Main thread is running...")
time.sleep(3)
print("Main thread is ending...")
Main thread is running...
Daemon thread is running...
Daemon thread is running...
Daemon thread is running...
Main thread is ending...
Daemon thread is running...
The is_alive Method
You can use the is_alive method to check whether a thread is currently running. It’s useful for one thread to monitor what another thread is doing, and especially handy for debugging and logging.
Example: Monitoring
import threading as th
import time
def monitor_thread(thread_to_monitor: th.Thread):
while thread_to_monitor.is_alive():
print(f"Monitoring: {thread_to_monitor.name} is running")
time.sleep(0.5)
print(f"Monitoring: {thread_to_monitor.name} has finished")
def worker():
print(f"{th.current_thread().name} started")
time.sleep(1)
print(f"{th.current_thread().name} finished")
if __name__ == "__main__":
work_thread = th.Thread(target=worker, name="WorkerThread")
monitor = th.Thread(
target=monitor_thread, args=(work_thread,), name="MonitorThread", daemon=True
)
work_thread.start()
monitor.start()
work_thread.join()
- Create work_thread first and pass it to the monitor thread.
- The monitor checks whether work_thread is running with
is_alive(). - When work_thread finishes, the main thread (MainThread) ends, and so does monitor, since it’s a daemon thread.
WorkerThread started
Monitoring: WorkerThread is running
Monitoring: WorkerThread is running
WorkerThread finished
Monitoring: WorkerThread has finished
Wrapping Up
So far, we’ve covered these important concepts:
- How a program goes from an executable file to being loaded into memory as a process
- The concept of a thread, the unit of execution that does the actual work inside a process
- How to create and manage threads in Python with the threading module
- Practical thread control techniques such as
join(),is_alive(), anddaemon
Going Further
For more efficient and safer multithreaded programming, it would be worth studying these more advanced topics. 😊
- Mutex
- Semaphore
- Efficient task processing with ThreadPoolExecutor