Python3 threading 模块

Python 的threading模块是实现多线程编程的核心工具,相比进程,线程更轻量、创建开销更低,且共享进程内存空间,适合 I/O 密集型任务(如网络请求、文件读写)的并发处理。本文从基础用法、线程同步、实战场景到核心坑点,全面拆解threading模块的使用逻辑,结合可运行示例,帮你掌握多线程编程的核心技巧。

一、核心认知:线程与 threading 模块的定位

1. 线程的特点(对比多进程)

  • 轻量级:线程创建 / 销毁的开销远低于进程,占用内存更少;
  • 共享内存:同一进程内的线程共享全局变量、文件句柄等资源,无需额外通信机制;
  • GIL 限制:Python 的全局解释器锁(GIL)导致同一时刻仅一个线程执行 Python 字节码,CPU 密集型任务无法通过多线程实现真正并行(需用multiprocessing),但 I/O 密集型任务仍能通过线程切换提升效率。

2. threading 模块的核心组件

组件作用
Thread 线程类,用于创建和管理线程
Lock/RLock 锁机制,解决资源竞争问题
Semaphore 信号量,控制并发线程数
Event 线程间事件通知
Condition 条件变量,实现复杂同步
Timer 定时线程,延迟执行任务

二、基础用法:创建与管理线程

threading模块创建线程有两种核心方式:传入目标函数(简单场景)、继承 Thread 类(复杂场景)。

1. 方式 1:传入 target 函数(最常用)

适用于逻辑简单的任务,直接将待执行的函数传入Thread对象,通过start()启动线程,join()等待线程结束。

示例:基础线程创建与执行

import threading
import time

# 定义线程执行的函数
def task(name, delay):
    print(f"线程{name}开始执行,延迟{delay}秒")
    time.sleep(delay)  # 模拟I/O操作(如网络请求)
    print(f"线程{name}执行完成")

if __name__ == "__main__":
    # 创建线程(daemon=False表示非守护线程,默认)
    t1 = threading.Thread(target=task, args=("A", 2))
    t2 = threading.Thread(target=task, args=("B", 1))

    # 启动线程(此时线程进入就绪状态,由系统调度执行)
    t1.start()
    t2.start()

    # 等待线程执行完成(主线程阻塞,直到t1、t2结束)
    print("主线程等待子线程完成...")
    t1.join()
    t2.join()
    print("所有线程执行完毕,主线程结束")
 

输出结果(线程执行顺序由系统调度,t2 可能先完成):

线程A开始执行,延迟2秒
线程B开始执行,延迟1秒
主线程等待子线程完成...
线程B执行完成
线程A执行完成
所有线程执行完毕,主线程结束
 

2. 方式 2:继承 Thread 类(自定义线程)

适用于需要扩展线程功能的场景(如重写__init__初始化参数、自定义run()方法)。

示例:自定义线程类 

import threading
import time

class MyThread(threading.Thread):
    def __init__(self, name, delay):
        super().__init__()  # 必须调用父类构造方法
        self.name = name
        self.delay = delay

    # 重写run()方法,线程启动后自动执行
    def run(self):
        print(f"自定义线程{self.name}启动,延迟{self.delay}秒")
        time.sleep(self.delay)
        print(f"自定义线程{self.name}结束")

if __name__ == "__main__":
    t1 = MyThread("C", 3)
    t2 = MyThread("D", 1)

    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print("自定义线程全部执行完毕")
 

3. 关键方法与属性

方法 / 属性作用
start() 启动线程(仅能调用一次,重复调用会报错)
join(timeout) 等待线程结束,timeout 为可选超时时间(秒)
is_alive() 判断线程是否处于活动状态(启动后、结束前为 True)
daemon 设置是否为守护线程(True 表示守护线程,主线程结束则子线程强制终止)
name 线程名称(可自定义,默认 Thread-1、Thread-2...)

守护线程示例(主线程结束,子线程强制终止): 

import threading
import time

def daemon_task():
    while True:
        print("守护线程运行中...")
        time.sleep(1)

if __name__ == "__main__":
    # 设置daemon=True,成为守护线程
    t = threading.Thread(target=daemon_task, daemon=True)
    t.start()
    time.sleep(3)  # 主线程休眠3秒
    print("主线程结束,守护线程被强制终止")
 
 
输出:
 
守护线程运行中...
守护线程运行中...
守护线程运行中...
主线程结束,守护线程被强制终止
 

三、线程同步:解决资源竞争问题

多线程共享内存时,若同时修改同一资源(如全局变量),会引发竞态条件(Race Condition),导致数据错误。threading模块提供多种同步工具,核心是Lock(锁)。

1. 问题重现:未加锁的资源竞争 

import threading

# 共享资源
count = 0

def increment():
    global count
    for _ in range(100000):
        count += 1  # 非原子操作(读取→加1→写入)

if __name__ == "__main__":
    t1 = threading.Thread(target=increment)
    t2 = threading.Thread(target=increment)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(f"最终count值:{count}")  # 预期200000,实际远小于该值
 

原因:count += 1不是原子操作,两个线程同时读取、修改 count,导致数据覆盖。

2. 解决方案 1:Lock(互斥锁)

Lock是最基础的同步工具,通过acquire()获取锁,release()释放锁,确保同一时刻仅一个线程操作共享资源。

加锁后的示例: 

import threading

count = 0
lock = threading.Lock()  # 创建锁对象

def increment():
    global count
    for _ in range(100000):
        lock.acquire()  # 获取锁(若锁被占用则阻塞)
        try:
            count += 1  # 临界区:仅一个线程能执行
        finally:
            lock.release()  # 确保锁最终释放(即使发生异常)

if __name__ == "__main__":
    t1 = threading.Thread(target=increment)
    t2 = threading.Thread(target=increment)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(f"最终count值:{count}")  # 准确输出200000
 

3. 其他同步工具

(1)RLock(可重入锁)

Lock不允许同一线程多次acquire()(会导致死锁),而RLock支持同一线程多次获取锁,需保证acquire()release()次数一致:
  
rlock = threading.RLock()
rlock.acquire()
rlock.acquire()  # 不会死锁
rlock.release()
rlock.release()
 

(2)Semaphore(信号量)

控制同时访问资源的线程数,适用于 “有限资源池” 场景(如最多 3 个线程同时访问数据库):
  
import threading
import time

sem = threading.Semaphore(3)  # 最多3个线程同时执行

def task(name):
    sem.acquire()
    print(f"线程{name}获取资源,开始执行")
    time.sleep(2)
    sem.release()
    print(f"线程{name}释放资源,执行完成")

if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=task, args=(i,))
        t.start()
 

(3)Event(事件通知)

实现线程间的 “等待 - 通知” 机制,通过set()设置事件、wait()等待事件、clear()清空事件:
  
import threading
import time

event = threading.Event()  # 初始为False

def wait_event():
    print("线程等待事件触发...")
    event.wait()  # 阻塞,直到event被set()
    print("事件触发,线程继续执行")

if __name__ == "__main__":
    t = threading.Thread(target=wait_event)
    t.start()
    time.sleep(3)
    print("主线程触发事件")
    event.set()  # 触发事件
    t.join()
 

四、实战场景:多线程处理 I/O 密集型任务

多线程最适合 I/O 密集型任务(如批量下载文件、接口请求),因为 I/O 等待时线程会释放 GIL,让其他线程执行。

示例:多线程下载图片 

import threading
import requests
import os

# 待下载的图片URL
img_urls = [
    "https://example.com/img1.jpg",
    "https://example.com/img2.jpg",
    "https://example.com/img3.jpg"
]
save_dir = "downloads"
os.makedirs(save_dir, exist_ok=True)

def download_img(url, idx):
    try:
        resp = requests.get(url, timeout=10)
        with open(f"{save_dir}/img{idx}.jpg", "wb") as f:
            f.write(resp.content)
        print(f"图片{idx}下载完成")
    except Exception as e:
        print(f"图片{idx}下载失败:{e}")

if __name__ == "__main__":
    threads = []
    for idx, url in enumerate(img_urls):
        t = threading.Thread(target=download_img, args=(url, idx+1))
        threads.append(t)
        t.start()
    
    # 等待所有下载线程完成
    for t in threads:
        t.join()
    print("所有图片下载完成")
 

五、核心坑点与避坑指南

1. GIL 的 “陷阱”

  • 误区:多线程能加速 CPU 密集型任务(如大规模计算);
  • 真相:GIL 限制下,多线程在 CPU 密集型任务中反而因线程切换增加开销,效率低于单线程;
  • 解决方案:CPU 密集型任务用multiprocessing(多进程),I/O 密集型用threadingasyncio

2. 线程安全问题

  • 避免在多线程中修改非线程安全的对象(如列表、字典,虽可修改但需加锁);
  • 优先使用queue.Queue(线程安全队列)实现线程间数据传递,而非直接共享列表:
     
    import queue
    import threading
    
    q = queue.Queue()  # 线程安全队列
    
    def producer():
        for i in range(5):
            q.put(i)
            print(f"生产数据:{i}")
    
    def consumer():
        while not q.empty():
            data = q.get()
            print(f"消费数据:{data}")
            q.task_done()  # 标记任务完成
    
    if __name__ == "__main__":
        t1 = threading.Thread(target=producer)
        t2 = threading.Thread(target=consumer)
        t1.start()
        t2.start()
        q.join()  # 等待队列所有任务完成
    
     
     

3. 死锁问题

  • 原因:多个线程互相等待对方持有的锁;
  • 避坑:① 按固定顺序获取锁;② 给acquire()设置超时时间;③ 尽量减少锁的嵌套。

4. 异常处理

  • 子线程的异常不会自动传递到主线程,需手动捕获:
    def task():
        try:
            # 业务逻辑
            1 / 0
        except Exception as e:
            print(f"线程异常:{e}")
    
     
     

六、进阶优化:线程池(ThreadPoolExecutor)

threading模块无原生线程池,推荐使用concurrent.futures.ThreadPoolExecutor(基于threading封装),简化线程管理,避免手动创建大量线程:
 
from concurrent.futures import ThreadPoolExecutor
import time

def task(name):
    print(f"线程池任务{name}开始")
    time.sleep(2)
    return f"任务{name}完成"

if __name__ == "__main__":
    # 创建线程池,最大线程数3
    with ThreadPoolExecutor(max_workers=3) as executor:
        # 提交任务并获取结果
        futures = [executor.submit(task, i) for i in range(5)]
        for future in futures:
            print(future.result())  # 获取任务返回值
    print("线程池所有任务完成")
 

总结

threading模块是 Python 多线程编程的核心,核心要点:
 
  1. 适用场景:I/O 密集型任务(网络、文件、数据库操作);
  2. 核心问题:共享资源需加锁(Lock/RLock),避免竞态条件;
  3. 避坑重点:GIL 不适合 CPU 密集型任务,守护线程需注意生命周期,避免死锁;
  4. 进阶用法:优先使用ThreadPoolExecutor管理线程池,简化开发。

posted on 2025-12-17 16:16  小陶coding  阅读(12)  评论(0)    收藏  举报