线程/threading
流程
导入threading模块去→创建线程对象→启动线程。
基础
多线程执行顺序不确定导致结果错误,需要加锁。
由于 Python 有 GIL(全局解释器锁),单个字节码操作是原子的,但像 ticket -= 1 这种"读取→计算→写回"多步操作仍会被打断,所以仍需自己加锁。
实例
基础
python
import threading
import time
# 定义线程要执行的函数
def worker(name, delay):
for i in range(3):
print(f"线程 {name} 正在运行: {i}")
time.sleep(delay)
print(f"线程 {name} 结束")
# 1. 创建线程对象
t1 = threading.Thread(target=worker, args=("A", 1))
t2 = threading.Thread(target=worker, args=("B", 0.5))
# 2. 启动线程
t1.start()
t2.start()
# 主线程等待子线程结束(可选)
t1.join()
t2.join()
print("所有线程执行完毕")
线程 A 正在运行: 0
线程 B 正在运行: 0
线程 B 正在运行: 1
线程 A 正在运行: 1
线程 B 正在运行: 2
线程 A 正在运行: 2
线程 B 结束
线程 A 结束
所有线程执行完毕
资源共享
资源共享,为保证运行正常需要加锁。
不加锁版
python
import threading
import time
ticket = 10 # 共享资源:10 张票
def sell(name):
global ticket
while ticket > 0:
time.sleep(0.01) # 模拟售票耗时
ticket -= 1
print(f"{name} 卖出一张,剩余: {ticket}")
t1 = threading.Thread(target=sell, args=("窗口A",))
t2 = threading.Thread(target=sell, args=("窗口B",))
t1.start()
t2.start()
t1.join()
t2.join()
print("最终剩余票数:", ticket)
窗口A 卖出一张,剩余: 9
窗口B 卖出一张,剩余: 8
窗口A 卖出一张,剩余: 7
窗口B 卖出一张,剩余: 6
窗口A 卖出一张,剩余: 5
窗口B 卖出一张,剩余: 4
窗口A 卖出一张,剩余: 3
窗口B 卖出一张,剩余: 2
窗口A 卖出一张,剩余: 1
窗口B 卖出一张,剩余: 0
窗口A 卖出一张,剩余: -1 ← 出现了负数!
最终剩余票数: -1
加锁版
lock.acquire(timeout=2) 可设置锁的超时时间,以秒为单位。
python
import threading
import time
ticket = 10
lock = threading.Lock() # 创建锁
def sell(name):
global ticket
while True:
with lock: # 加锁,保证同一时刻只有一个线程操作
if ticket <= 0:
break
time.sleep(0.01)
ticket -= 1
print(f"{name} 卖出一张,剩余: {ticket}")
t1 = threading.Thread(target=sell, args=("窗口A",))
t2 = threading.Thread(target=sell, args=("窗口B",))
t1.start()
t2.start()
t1.join()
t2.join()
print("最终剩余票数:", ticket)
窗口A 卖出一张,剩余: 9
窗口B 卖出一张,剩余: 8
窗口B 卖出一张,剩余: 7
窗口A 卖出一张,剩余: 6
窗口A 卖出一张,剩余: 5
窗口B 卖出一张,剩余: 4
窗口B 卖出一张,剩余: 3
窗口A 卖出一张,剩余: 2
窗口A 卖出一张,剩余: 1
窗口B 卖出一张,剩余: 0
最终剩余票数: 0
使用try...finally
python
import threading
import time
ticket = 10
lock = threading.Lock()
def sell(name):
global ticket
while True:
lock.acquire() # 手动加锁
try:
if ticket <= 0:
break
time.sleep(0.01)
ticket -= 1
print(f"{name} 卖出一张,剩余: {ticket}")
finally:
lock.release() # 无论是否异常,都释放锁
t1 = threading.Thread(target=sell, args=("窗口A",))
t2 = threading.Thread(target=sell, args=("窗口B",))
t1.start()
t2.start()
t1.join()
t2.join()
print("最终剩余票数:", ticket)
窗口A 卖出一张,剩余: 9
窗口B 卖出一张,剩余: 8
窗口B 卖出一张,剩余: 7
窗口A 卖出一张,剩余: 6
窗口A 卖出一张,剩余: 5
窗口B 卖出一张,剩余: 4
窗口B 卖出一张,剩余: 3
窗口A 卖出一张,剩余: 2
窗口A 卖出一张,剩余: 1
窗口B 卖出一张,剩余: 0
最终剩余票数: 0
总结
减少锁的范围,只给"修改共享资源的代码块"上锁,不要给整个函数上锁,会影响效率。
避免多把锁,尽量用一把锁解决问题,多把锁会导致死锁。
| 写法 | 优点 | 缺点 |
|---|---|---|
| with lock: | 简洁、自动释放、异常安全 | 不够直观看到加解锁 |
| try...finally | 显式控制,逻辑清晰 | 代码稍长,容易忘记写 finally |
with lock: 本质上就是 try...finally 的语法糖,日常推荐用 with;但理解 try...finally 有助于看懂底层原理和排查死锁。