什么是线程?
想象你在一家餐厅工作:
💡 进程就像是整个餐厅 – 包含厨房、用餐区、收银台等
💡 线程就像是餐厅里的服务员 – 每个服务员可以同时处理多个任务(点餐、上菜、结账)
在计算机中:
- 一个进程是程序执行的一个实例,拥有独立的内存空间
- 线程是进程内的执行单元,所有线程共享相同的内存空间
- 线程比进程更轻量级,创建和切换开销更小
- Python的
threading
模块提供了操作线程的接口
为什么使用线程?
线程主要用于以下场景:
1. 提高程序响应性
在GUI程序中,用一个线程处理用户界面,另一个线程执行耗时操作,这样界面不会”卡死”
2. 充分利用I/O等待时间
当程序需要等待网络响应或文件读写时,CPU可以切换到其他线程执行任务
3. 多核CPU并行计算
虽然Python有GIL限制,但在I/O密集型任务中仍然能利用多核优势
但要注意:
- 线程不适合CPU密集型任务(因为GIL限制)
- 线程间共享数据可能导致竞争条件
- 线程调试比单线程复杂
创建线程的两种方式
方法1:直接创建Thread对象
import threading
import time
# 定义线程要执行的任务
def task(name):
print(f”线程 {name} 开始工作”)
time.sleep(2) # 模拟耗时操作
print(f”线程 {name} 工作完成”)
# 创建线程对象
thread1 = threading.Thread(target=task, args=(“A”,))
thread2 = threading.Thread(target=task, args=(“B”,))
# 启动线程
thread1.start()
thread2.start()
# 等待线程结束
thread1.join()
thread2.join()
print(“所有线程执行完毕”)
方法2:继承Thread类
class MyThread(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f”线程 {self.name} 开始工作”)
time.sleep(2)
print(f”线程 {self.name} 工作完成”)
# 使用自定义线程类
thread1 = MyThread(“A”)
thread2 = MyThread(“B”)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
线程的生命周期
(图示:线程从创建到结束的状态变化)
状态 |
说明 |
转换方法 |
新建(New) |
线程被创建但未启动 |
threading.Thread() |
就绪(Ready) |
线程可以运行,等待CPU时间片 |
start()方法调用后 |
运行(Running) |
线程正在执行代码 |
获得CPU时间片 |
阻塞(Blocked) |
线程等待I/O操作或同步锁 |
sleep(), wait(), acquire()等 |
终止(Dead) |
线程执行完毕或异常退出 |
run()方法结束 |
线程同步机制
当多个线程访问共享资源时,可能出现竞争条件,导致数据不一致。
📖 类比:多个厨师同时使用同一个食谱
如果不加控制,厨师A可能正在添加食材时,厨师B修改了食谱,导致混乱
Python提供的同步工具:
- Lock:互斥锁,确保一次只有一个线程访问资源
- RLock:可重入锁,同一个线程可以多次获取
- Semaphore:信号量,控制同时访问资源的线程数量
- Event:事件,线程间通信机制
- Condition:条件变量,用于复杂的线程同步
线程锁(Lock)
锁是最基本的同步机制,用于保护共享资源
import threading
# 共享资源
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
# 获取锁
lock.acquire()
try:
counter += 1
finally:
# 释放锁
lock.release()
# 创建多个线程
threads = []
for i in range(5):
t = threading.Thread(target=increment)
threads.append(t)
t.start()
# 等待所有线程完成
for t in threads:
t.join()
print(f”最终计数器值: {counter}”) # 应该是500000
⚠️ 注意:使用锁时要注意避免死锁,即两个线程互相等待对方释放锁
使用with
语句可以简化锁的使用:
def increment_safe():
global counter
for _ in range(100000):
with lock:
counter += 1
线程队列(Queue)
队列是线程间安全通信的最佳方式
📦 类比:传送带系统
生产者把产品放在传送带(队列)上,消费者从传送带上取产品
import threading
import queue
import time
# 创建线程安全的队列
q = queue.Queue(maxsize=5)
def producer():
for i in range(10):
item = f”产品-{i}”
q.put(item)
print(f”生产者生产: {item}”)
time.sleep(0.5)
def consumer():
while True:
item = q.get()
if item is None: # 收到终止信号
break
print(f”消费者消费: {item}”)
time.sleep(1)
q.task_done()
# 创建生产者和消费者线程
prod_thread = threading.Thread(target=producer)
cons_thread = threading.Thread(target=consumer)
prod_thread.start()
cons_thread.start()
# 等待生产者完成
prod_thread.join()
# 发送结束信号
q.put(None)
cons_thread.join()
队列的常用方法:
put(item)
:添加元素
get()
:获取元素
task_done()
:通知任务完成
join()
:等待队列中所有任务完成
守护线程(Daemon Thread)
👼 类比:后台服务人员
当所有非守护线程(主线程)结束时,守护线程会自动退出
def background_task():
while True:
print(“守护线程运行中…”)
time.sleep(1)
# 创建守护线程
daemon_thread = threading.Thread(target=background_task)
daemon_thread.daemon = True # 设置为守护线程
daemon_thread.start()
# 主线程工作
print(“主线程开始工作”)
time.sleep(3)
print(“主线程结束”)
# 此时守护线程会自动终止
⚠️ 注意:
- 守护线程会在主线程结束时立即终止,不会执行清理操作
- 守护线程创建的子线程也是守护线程
- 适合用于不重要的后台任务,如日志、监控等
线程局部数据(Thread-local Data)
每个线程拥有自己独立的数据副本
🎒 类比:每个学生有自己的书包
虽然都在同一个教室(进程)里,但每个学生(线程)的书包(数据)是独立的
import threading
# 创建线程局部数据
local_data = threading.local()
def show_data():
print(f”线程 {threading.current_thread().name} 的值: {local_data.value}”)
def thread_task(value):
# 每个线程设置自己的值
local_data.value = value
show_data()
# 创建多个线程
threads = []
for i in range(3):
t = threading.Thread(target=thread_task, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
输出结果:
线程 Thread-1 的值: 0
线程 Thread-2 的值: 1
线程 Thread-3 的值: 2
GIL全局解释器锁
Python多线程的最大限制
GIL(Global Interpreter Lock)是CPython解释器的特性:
- 任何时候只有一个线程在执行Python字节码
- 防止多线程同时访问Python对象导致状态不一致
对多线程编程的影响:
任务类型 |
GIL影响 |
建议方案 |
I/O密集型 |
影响小 |
多线程很有效 |
CPU密集型 |
影响大 |
使用多进程(multiprocessing) |
虽然GIL存在,但在以下情况多线程仍有用:
- 网络请求、文件I/O等操作会释放GIL
- 使用C扩展可以绕过GIL
- Python 3.2+改进了GIL实现,减少了对性能的影响
使用线程的注意事项
1. 避免过度使用线程
线程不是越多越好,过多的线程会增加上下文切换开销
2. 优先使用队列通信
队列比共享变量更安全,减少同步问题
3. 注意资源竞争
对共享资源的访问要加锁保护
4. 避免死锁
按固定顺序获取锁,使用超时机制
5. 区分CPU密集和I/O密集
CPU密集型任务考虑使用多进程
总结:何时使用多线程
✅ 处理I/O密集型任务(网络请求、文件读写)
✅ 需要保持用户界面响应
✅ 执行后台任务(日志、监控)
❌ 避免用于CPU密集型计算