目录
[Python 线程同步与异步编程 全解析](#Python 线程同步与异步编程 全解析)
[1. 为什么需要线程同步?](#1. 为什么需要线程同步?)
[2. 常用线程同步工具(附代码示例)](#2. 常用线程同步工具(附代码示例))
[(1)互斥锁 Lock(最常用)](#(1)互斥锁 Lock(最常用))
[(2)可重入锁 RLock](#(2)可重入锁 RLock)
[(3)条件变量 Condition](#(3)条件变量 Condition)
[(4)信号量 Semaphore](#(4)信号量 Semaphore)
[(5)事件 Event](#(5)事件 Event)
[二、Python 异步编程(asyncio)](#二、Python 异步编程(asyncio))
[1. 异步核心原理](#1. 异步核心原理)
[2. 核心关键字](#2. 核心关键字)
[3. 基础示例](#3. 基础示例)
[4. 异步 vs 多线程 核心对比](#4. 异步 vs 多线程 核心对比)
Python 线程同步与异步编程 全解析
先给核心概念大白话比喻,彻底搞懂同步、异步、多线程的区别:
- 同步:排队执行,一个任务完全结束后,下一个任务才能开始,全程按顺序来,不会穿插。
- 异步:任务穿插执行,一个任务遇到「等待 / 阻塞」(比如等网络请求、等文件读写)时,先把 CPU 让给其他任务,等阻塞结束再回来继续执行,不用干等。
- 线程同步:多线程场景下,为了避免多个线程同时操作共享数据导致出错,加锁让线程排队访问共享资源,保证数据安全。
- 异步编程:单线程内的并发方案,靠「协程 + 事件循环」实现任务切换,不用开多线程,就能处理多个 IO 密集型任务。
一、线程同步(多线程场景)
1. 为什么需要线程同步?
多线程场景下,多个线程会同时操作同一份共享数据,如果不加控制,会出现「数据竞争」,导致结果错误、数据混乱。
经典的「卖票超卖」问题示例(无同步的错误场景):
python
import threading
import time
# 共享数据:总票数
ticket = 10
def sell_ticket():
global ticket
while ticket > 0:
# 模拟卖票的IO操作
time.sleep(0.1)
ticket -= 1
print(f"线程{threading.current_thread().name} 卖出1张票,剩余{ticket}张")
# 开启3个卖票线程
for i in range(3):
t = threading.Thread(target=sell_ticket, name=f"售票员{i+1}")
t.start()
错误结果 :会出现剩余票数为负数、多个线程卖出同一张票的情况,因为多个线程同时读取了ticket=1,都执行了ticket-=1。
2. 常用线程同步工具(附代码示例)
(1)互斥锁 Lock(最常用)
- 核心作用:保证同一时间只有一个线程能获取到锁,只有获取到锁的线程能执行临界区代码(操作共享数据的代码),其他线程会阻塞等待。
- 核心方法:
acquire()获取锁、release()释放锁,推荐用with语句自动管理锁(避免忘记释放导致死锁)。
python
import threading
import time
ticket = 10
# 创建锁对象
lock = threading.Lock()
def sell_ticket():
global ticket
while True:
# 1. 获取锁:如果锁被其他线程占用,当前线程阻塞等待
lock.acquire()
# 2. 临界区:操作共享数据,只有获取到锁的线程能执行
if ticket > 0:
time.sleep(0.1)
ticket -= 1
print(f"线程{threading.current_thread().name} 卖出1张票,剩余{ticket}张")
else:
# 无票时释放锁,退出循环
lock.release()
break
# 3. 释放锁:其他线程可以获取锁执行
lock.release()
# 推荐写法:with语句自动获取/释放锁,不用手动写release()
def sell_ticket_with():
global ticket
while True:
# with块内自动获取锁,执行完自动释放锁
with lock:
if ticket > 0:
time.sleep(0.1)
ticket -= 1
print(f"线程{threading.current_thread().name} 卖出1张票,剩余{ticket}张")
else:
break
# 开启3个卖票线程
for i in range(3):
t = threading.Thread(target=sell_ticket_with, name=f"售票员{i+1}")
t.start()
正确结果:不会出现超卖,票数从 10 依次减到 0,线程有序卖票。
(2)可重入锁 RLock
- 核心作用:同一个线程可以多次获取同一把锁,不会出现死锁,适合递归调用、嵌套加锁的场景。
- 注意:
Lock是不可重入的,同一个线程连续两次acquire()会直接死锁。
python
import threading
# 创建可重入锁
rlock = threading.RLock()
def func1():
rlock.acquire()
print("func1获取了锁")
# 同一个线程再次获取锁,不会死锁
func2()
rlock.release()
def func2():
rlock.acquire()
print("func2也获取了锁")
rlock.release()
t = threading.Thread(target=func1)
t.start()
(3)条件变量 Condition
- 核心作用:实现线程间的「等待 / 通知」机制,适合生产者 - 消费者模型,一个线程生产数据,通知其他线程消费;没有数据时,消费线程等待。
- 核心方法:
wait()让线程等待、notify()唤醒一个等待线程、notify_all()唤醒所有等待线程。
python
import threading
import time
from collections import deque
# 共享队列
queue = deque()
# 创建条件变量,内部自带锁
cond = threading.Condition()
# 生产者线程
def producer():
for i in range(5):
# 获取锁
with cond:
# 生产数据
queue.append(f"产品{i}")
print(f"生产了产品{i}")
# 唤醒一个等待的消费者线程
cond.notify()
time.sleep(1)
# 消费者线程
def consumer():
while True:
with cond:
# 队列空就等待,释放锁,让生产者可以获取锁生产
while not queue:
cond.wait()
# 有数据就消费
product = queue.popleft()
print(f"消费了{product}")
# 消费完唤醒生产者
cond.notify()
if not queue and product == "产品4":
break
# 开启线程
threading.Thread(target=producer).start()
threading.Thread(target=consumer).start()
(4)信号量 Semaphore
- 核心作用:控制同时访问某个资源的线程数量,类似限流,比如最多允许 3 个线程同时访问数据库。
- 核心方法:
acquire()获取信号量(计数器 - 1)、release()释放信号量(计数器 + 1),计数器为 0 时,后续线程阻塞等待。
python
import threading
import time
# 最多允许2个线程同时访问
semaphore = threading.Semaphore(2)
def access_database(thread_id):
semaphore.acquire()
print(f"线程{thread_id} 开始访问数据库")
time.sleep(2)
print(f"线程{thread_id} 结束访问数据库")
semaphore.release()
# 开启5个线程
for i in range(5):
threading.Thread(target=access_database, args=(i,)).start()
结果:同一时间最多只有 2 个线程在访问数据库,其他线程等待。
(5)事件 Event
- 核心作用:线程间的「信号通知」,一个线程设置事件信号,其他线程等待事件信号,实现线程间的同步触发。
- 核心方法:
set()设置事件为 True、clear()设置事件为 False、wait()等待事件为 True。
python
import threading
import time
# 创建事件对象,默认状态为False
event = threading.Event()
# 等待事件的线程
def wait_event():
print("等待事件触发...")
# 阻塞等待,直到事件被set为True
event.wait()
print("事件触发,开始执行任务!")
# 触发事件的线程
def trigger_event():
time.sleep(3)
print("准备触发事件...")
# 设置事件为True,唤醒所有等待的线程
event.set()
# 开启线程
threading.Thread(target=wait_event).start()
threading.Thread(target=trigger_event).start()
二、Python 异步编程(asyncio)
1. 异步核心原理
异步是单线程内的并发方案,核心是「协程 + 事件循环」:
- 把一个大任务拆分成多个小的协程任务,遇到 IO 阻塞(网络请求、文件读写、sleep)时,自动把 CPU 切给其他协程任务,不用干等 IO 完成,实现单线程内的并发。
- 适合IO 密集型任务(比如爬虫、接口请求、文件读写),比多线程开销更小,没有线程安全问题。
2. 核心关键字
表格
| 关键字 | 作用 |
|---|---|
async |
定义协程函数,调用后不会执行,会返回一个协程对象 |
await |
挂起当前协程,等待 IO 操作完成,同时把 CPU 让给其他协程 |
asyncio.run() |
运行协程,启动事件循环 |
3. 基础示例
python
import asyncio
import time
# 定义协程函数
async def task(name, delay):
print(f"任务{name} 开始执行")
# 模拟IO阻塞,await会挂起当前协程,让CPU给其他任务
await asyncio.sleep(delay)
print(f"任务{name} 执行完成,耗时{delay}秒")
# 运行协程
async def main():
# 串行执行:一个任务结束再执行下一个,总耗时3+2=5秒
print("===== 串行执行 =====")
start = time.time()
await task("A", 3)
await task("B", 2)
print(f"串行总耗时:{time.time() - start:.2f}秒")
# 并发执行:两个任务同时启动,总耗时取最大值3秒
print("\n===== 并发执行 =====")
start = time.time()
# 并发运行多个协程
await asyncio.gather(
task("A", 3),
task("B", 2)
)
print(f"并发总耗时:{time.time() - start:.2f}秒")
# 启动异步程序
asyncio.run(main())
输出结果:
plaintext
===== 串行执行 =====
任务A 开始执行
任务A 执行完成,耗时3秒
任务B 开始执行
任务B 执行完成,耗时2秒
串行总耗时:5.01秒
===== 并发执行 =====
任务A 开始执行
任务B 开始执行
任务B 执行完成,耗时2秒
任务A 执行完成,耗时3秒
并发总耗时:3.01秒
4. 异步 vs 多线程 核心对比
表格
| 对比维度 | 多线程 | 异步编程 |
|---|---|---|
| 并发原理 | 多个线程(多个执行流),由操作系统调度切换 | 单线程内的协程,由用户程序自己调度切换 |
| 开销 | 线程创建、切换开销大 | 协程开销极小,轻量级 |
| 适用场景 | CPU 密集型任务(计算量大)、IO 密集型任务 | 仅适合 IO 密集型任务,CPU 密集型任务会阻塞整个事件循环 |
| 线程安全 | 多线程同时操作共享数据,需要加锁同步 | 单线程内执行,不会同时操作共享数据,无线程安全问题 |
| 编程复杂度 | 锁、条件变量等同步机制复杂 | 逻辑简单,没有锁的问题 |
三、适用场景总结
-
用线程同步(多线程)的场景:
- 有大量计算的 CPU 密集型任务,需要多核并行加速
- 多个独立的任务,需要同时执行,且有共享数据需要保护
- 注意:Python 的 GIL 锁会限制多线程的 CPU 并行效果,CPU 密集型任务更推荐用
multiprocessing多进程
-
用异步编程的场景:
- 大量 IO 密集型任务,比如网络请求、文件读写、数据库操作
- 任务之间有等待关系,不需要同时计算,只需要等待 IO 完成
- 高并发场景,比如 Web 接口、爬虫,用异步可以用少量线程处理大量请求
-
简单选择规则:
- 任务里大部分是「等」(等网络、等文件)→ 用异步
- 任务里大部分是「算」(计算、循环)→ 用多进程 / 多线程
- 多个任务需要操作同一份数据 → 用多线程 + 锁同步