Python 线程同步与异步编程 全解析

目录

[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 密集型任务会阻塞整个事件循环
线程安全 多线程同时操作共享数据,需要加锁同步 单线程内执行,不会同时操作共享数据,无线程安全问题
编程复杂度 锁、条件变量等同步机制复杂 逻辑简单,没有锁的问题

三、适用场景总结

  1. 用线程同步(多线程)的场景

    • 有大量计算的 CPU 密集型任务,需要多核并行加速
    • 多个独立的任务,需要同时执行,且有共享数据需要保护
    • 注意:Python 的 GIL 锁会限制多线程的 CPU 并行效果,CPU 密集型任务更推荐用multiprocessing多进程
  2. 用异步编程的场景

    • 大量 IO 密集型任务,比如网络请求、文件读写、数据库操作
    • 任务之间有等待关系,不需要同时计算,只需要等待 IO 完成
    • 高并发场景,比如 Web 接口、爬虫,用异步可以用少量线程处理大量请求
  3. 简单选择规则

    • 任务里大部分是「等」(等网络、等文件)→ 用异步
    • 任务里大部分是「算」(计算、循环)→ 用多进程 / 多线程
    • 多个任务需要操作同一份数据 → 用多线程 + 锁同步
相关推荐
吃饱了得干活1 小时前
缓存与数据库一致性:从理论到实战
java·后端·面试
520拼好饭被践踏1 小时前
JAVA+Agent学习day22
java·开发语言·后端·学习
笨蛋不要掉眼泪1 小时前
Java虚拟机:堆的参数配置
java·开发语言·jvm
霸道流氓气质2 小时前
SpringBoot中使用JasperReports 报表引擎 — 介绍、原理与使用实践
java·spring boot·后端
冰暮流星2 小时前
javascript之date对象
开发语言·javascript·ecmascript
花生了什么事o2 小时前
DDD 分层架构:六层分层架构
java·架构·ddd
Y3815326622 小时前
3 种 SERP 数据处理方案对比:流式 / 批处理 / 缓存
java·spring·缓存
Wang's Blog2 小时前
Go-Zero项目开发17: IM私聊功能实现与消息存储设计
开发语言·后端·golang
Ulyanov2 小时前
Python雷达电子对抗仿真引擎(一):打破单体瓶颈,构建微服务与ECS架构的顶层设计
开发语言·python·微服务·云原生·架构·雷达电子对抗