多线程编程
并行与并发

- 并发:指一段时间内交替执行任务。(任务数多于CPU核数时)
- 并行:一段时间任务同时进行(任务数小于CPU核数时)
进程与线程

threading 库是 Python 中用于多线程编程的标准库,允许创建和管理线程,从而实现并发执行任务。多线程适用于 I/O 密集型任务(如文件读写、网络请求等),但不适合 CPU 密集型任务(因为 Python 的全局解释器锁 GIL 会限制多线程的并行执行)。
基本概念
线程:线程是操作系统能够调度的最小单位,一个进程可以包含多个线程,共享进程的内存空间。
GIL(全局解释器锁):Python 的 GIL 会限制同一时间只有一个线程执行 Python 字节码,因此多线程在 CPU 密集型任务中无法实现真正的并行。
线程安全:多个线程同时访问共享资源时,可能会导致数据不一致问题,需要使用锁(Lock)等机制来保证线程安全。
基本使用方法
- Thread类是threading的核心
python
# Thread参数
threading.Thread(
target=None, # 线程要执行的函数
name=None, # 线程的名称
args=(), # 传递给目标函数的参数(元组)
kwargs={}, # 传递给目标函数的关键字参数(字典)
daemon=None # 是否设置为守护线程
)
- 常用方法
- start():启动线程,调用线程的 run() 方法。
- run():线程执行的主体方法,可以重写。
- join(timeout=None):等待线程结束。timeout 是超时时间(秒),超时后继续执行主线程。
- is_alive():返回线程是否仍在运行。
- name:获取或设置线程的名称。
- daemon:获取或设置线程是否为守护线程。
创建线程
- 直接传入目标函数
python
import threading
def worker():
print("Worker thread is running")
# 创建线程
thread = threading.Thread(target=worker)
# 启动线程
thread.start()
# 等待线程结束
thread.join()
print("Main thread finished")
- 继承Thread重写run()
python
import threading
class MyThread(threading.Thread):
def run(self):
print("Worker thread is running")
# 创建线程
thread = MyThread()
# 启动线程
thread.start()
# 等待线程结束
thread.join()
print("Main thread finished")
线程的参数传递
- 如果需要向线程函数传递参数,可以使用 args 或 kwargs。
python
import threading
def worker(name, delay):
print(f"{name} is running")
import time
time.sleep(delay)
print(f"{name} is done")
# 创建线程并传递参数
thread = threading.Thread(target=worker, args=("Thread-1", 2))
# 启动线程
thread.start()
# 等待线程结束
thread.join()
print("Main thread finished")
线程同步(防止资源竞争)
多个线程同时访问共享资源时,可能会导致数据不一致问题。可以使用锁(Lock)来保证线程安全。
- 没有使用线程输出正常
python
a = 0
b = 10000000
def sum1():
for i in range(b):
global a
a += 1
print(a)
def sum2():
for i in range(b):
global a
a += 1
print(a)
if __name__ == '__main__':
sum1() # 输出 10000000
sum2() # 输出 20000000
- 使用线程
python
from threading import Thread
a = 0
b = 10000000
def sum1():
for i in range(b):
global a
a += 1
print(a)
def sum2():
for i in range(b):
global a
a += 1
print(a)
if __name__ == '__main__':
t1 = Thread(target=sum1)
t2 = Thread(target=sum2)
t1.start() # 输出 12371445
t2.start() # 输出 13385086
这里的输出结果明显不一样了,用多线程对全局变量 a 进行累加操作,最终输出的结果并不是预期的 20000000,这是由于多线程环境下的竞态条件(Race Condition) 所导致的。
- 在 Python 中,a += 1 这一操作并不是原子操作。原子操作是指在执行过程中不会被其他线程中断的操作。而 a += 1 实际上包含了三个步骤:
- 读取:从内存中读取 a 的当前值。
- 计算:将读取的值加 1。
- 写入:将计算结果写回到内存中。
- 当两个线程同时对 a 进行操作时,可能会出现以下情况:
- 线程 t1 读取了 a 的值,假设为 x。
- 线程 t2 也读取了 a 的值,由于线程 t1 还没有来得及将计算结果写回内存,所以线程 t2 读取到的值也是 x。
- 线程 t1 将 x 加 1 得到 x + 1,并将其写回到内存中。
- 线程 t2 同样将 x 加 1 得到 x + 1,并将其写回到内存中。
这样,虽然两个线程都执行了一次累加操作,但实际上 a 的值只增加了 1,而不是 2。这种情况会多次发生,导致最终结果小于预期值。
- 解决资源竞争
- 加锁(Lock)来保证线程的安全
python
from threading import Thread, Lock
a = 0
b = 10000000
lock = Lock()
def sum1():
lock.acquire()
for i in range(b):
global a
a += 1
print(a)
lock.release()
def sum2():
lock.acquire()
for i in range(b):
global a
a += 1
print(a)
lock.release()
if __name__ == '__main__':
t1 = Thread(target=sum1)
t2 = Thread(target=sum2)
t1.start() # 输出 10000000
t2.start() # 输出 20000000
- join()等待线程结束
python
from threading import Thread
a = 0
b = 10000000
def sum1():
for i in range(b):
global a
a += 1
print(a)
def sum2():
for i in range(b):
global a
a += 1
print(a)
if __name__ == '__main__':
t1 = Thread(target=sum1)
t2 = Thread(target=sum2)
t1.start() # 输出 10000000
t1.join()
t2.start() # 输出 20000000
- threading.Lock 类
- Lock 类用于实现线程同步,确保多个线程不会同时访问共享资源。
- 常用方法
acquire(blocking=True, timeout=-1):获取锁。(blocking:如果为 True,线程会阻塞直到获取锁;如果为 False,线程会立即返回是否成功获取锁。timeout:阻塞的最长时间(秒),超时后返回 False。)
release():释放锁。
locked():返回锁是否被占用。
- threading.RLock 类
- RLock(可重入锁)是 Lock 的增强版,允许同一个线程多次获取锁。
常用方法
与 Lock 相同,但支持重入。
- RLock(可重入锁)是 Lock 的增强版,允许同一个线程多次获取锁。
守护线程
守护线程(Daemon Thread)是一种在后台运行的线程,当主线程退出时,守护线程会自动退出。
python
import threading
import time
def daemon_worker():
print("Daemon thread started")
time.sleep(2)
print("Daemon thread finished") # 可能不会执行,因为主线程退出了
# 创建守护线程
thread = threading.Thread(target=daemon_worker)
thread.daemon = True # 设置为守护线程
# 启动线程
thread.start()
# 主线程退出
print("Main thread finished")
- 输出
python
# 正常输出应该是
Daemon thread started
Daemon thread finished
Main thread finished
# 实际输出为
Daemon thread started
Main thread finished
原因是因为守护线程中有一个 time.sleep(2)函数,当代码运行时,主线先启动线程 thread.start()
再执行 print("Main thread finished"),再运行 thread.start()中由于有两秒睡眠时间,但是主线程并不会等待守护线程运行再运行,他们之间是并行的关系,主线程结束时守护线程还没有睡眠结束自然不会执行 print("Daemon thread finished")如果删去time.sleep(2)函数则会正常输出。
-
常用方法
threading.Thread(target, args, kwargs):创建线程。
thread.start():启动线程。
thread.join():等待线程结束。
threading.Lock():创建锁,用于线程同步。
threading.current_thread():获取当前线程对象。
threading.active_count():获取当前活动的线程数。