Python 并发 线程

线程/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 有助于看懂底层原理和排查死锁。

相关推荐
晨曦中的暮雨1 小时前
限流算法——固定窗口、滑动窗口、漏桶、令牌桶
java·开发语言·算法
喵了几个咪2 小时前
GoWind CMS 拆解:在 Go 里给 TipTap 搭一套 Yjs 协同后端
开发语言·后端·golang
程序员清风2 小时前
数据处理脚本工程化:日志、配置、命令行与测试
服务器·开发语言·数据库
在世修行4 小时前
智学文枢:页面切换与跨页数据预填
python·桌面应用·signal·1024程序员节·pyside·跨页跳转·状态预填
2601_962885724 小时前
如何用 Python 把 K 线重采样成任意周期?
开发语言·python
打工仔折腾 AI4 小时前
在 Ubuntu ARM64 部署 Q1 V3.0 MuJoCo 模型:加载、单步推理与执行器响应解析
linux·运维·人工智能·python·深度学习·ubuntu·性能优化
Wang's Blog4 小时前
Java 项目实战: 交友项目 - 圈子功能实现:总结
java·开发语言·交友
信誓旦旦的程序猿5 小时前
【Python量化实战宝典 #21】北向资金跟随手册:聪明钱择时信号(Python 可跑)
python·1024程序员节·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
严谨的麻辣烫5 小时前
静态 ISP 代理请求超时怎么排查?Python 与 curl 实战
python·http·requests·1024程序员节·网络排障
下一站春风里5 小时前
位运算完整学习笔记:按位与(&)+ 异或(^)
开发语言·c++·算法