Python 并发编程系统笔记

1. 串行、并行与并发

1.1 串行(Serial)

What(是什么)

想象你在排队买奶茶,前面的人点完单、付完钱、拿到奶茶走了,才轮到你。你后面的人也必须等你完成全部步骤后才能上前。这就是串行------事情一件一件按顺序做,前一件彻底完成后,下一件才能开始。

在编程里,串行就是让CPU把第一个任务完全执行完,再去执行第二个任务,再执行第三个......一个接一个,绝不插队。

Why(为什么要了解它)

这是最自然、最简单的做事方式。如果你只有一个人(一个CPU),而且后面的任务必须依赖前面任务的结果(比如你要先打开文件,才能读取内容;先做饭,才能吃饭),那就必须串行。理解串行是理解其他复杂方式的基础。

How(怎么用)

直接按顺序写代码就可以了,没有任何特殊语法:

python 复制代码
import time

def task1():
    print("第一个任务开始了")
    time.sleep(2)  # 假装这个任务需要2秒钟
    print("第一个任务完成了")

def task2():
    print("第二个任务开始了")
    time.sleep(2)
    print("第二个任务完成了")

# 按顺序调用
task1()  # 先执行第一个
task2()  # 再执行第二个
print("所有任务都完成了")
  • 业务场景:顺序处理文件、生成连续的编号
  • Web后端重要性:⭐(只是基础概念,实际开发很少单独用)
  • 面试:了解即可

1.2 并行(Parallelism)

What(是什么)

还是买奶茶的例子。这次奶茶店有3个店员,每个人都能独立工作。你和另外两个朋友同时走到3个不同的店员面前,同时点单、同时付钱、同时拿到奶茶。这就是并行------多个任务真的同时在进行。

在编程里,并行需要你的电脑有多个CPU核心(现在的电脑一般都是4核、8核甚至更多)。每个核心就像一个独立的店员,可以各自处理一个任务,真正的同时工作。

Why(为什么会这样)

早期电脑只有一个CPU核心,就像只有一个店员。后来人们发现,把很多个CPU核心放在一起,就能同时干更多活,速度直接翻倍。但前提是,这些任务之间不能互相依赖------你不能让一个任务等另一个任务的结果,不然就没法同时进行了。

How(怎么用)

在Python中,需要用到multiprocessing模块来创建多个"进程",操作系统会自动把不同进程分配到不同的CPU核心上:

python 复制代码
import multiprocessing
import time

def make_tea(name):
    print(f"{name}开始泡茶")
    time.sleep(2)  # 泡茶需要2秒
    print(f"{name}的茶泡好了")

def make_coffee(name):
    print(f"{name}开始冲咖啡")
    time.sleep(2)  # 冲咖啡需要2秒
    print(f"{name}的咖啡冲好了")

if __name__ == '__main__':
    # 创建两个进程,它们会同时运行
    worker1 = multiprocessing.Process(target=make_tea, args=("小明",))
    worker2 = multiprocessing.Process(target=make_coffee, args=("小红",))

    # 启动进程(两个同时开始)
    worker1.start()
    worker2.start()

    # 等待两个进程都完成
    worker1.join()
    worker2.join()

    print("茶和咖啡都好了!")
    # 总耗时约2秒,而不是4秒
  • 业务场景:批量处理图片、训练AI模型、大量计算任务
  • Web后端重要性:⭐⭐(后台定时计算任务会用到)
  • 面试:★(常问和并发的区别)

1.3 并发(Concurrency)

What(是什么)

这次你一个人在奶茶店工作,但突然来了3个客人。你不可能同时服务他们(因为只有一双手),但你很聪明------你先帮A客人点单,在等收银机反应的时候,转头去帮B客人点单,再让A付钱,同时帮C点单......你快速地在3个客人之间切换,每个人都觉得你在服务他们,但实际上你只是在很短的时间内不断切换。这就是并发------看起来像同时进行,实际上是在快速切换。

在编程里,当一个CPU核心要处理很多任务时,操作系统会把时间切成很小很小的碎片(可能每片只有几毫秒),一会儿执行任务A,一会儿执行任务B,一会儿执行任务C。因为切换得实在太快了,你根本感觉不到,从外面看就好像在同时执行一样。

Why(为什么要这样)

你的电脑不可能为每一个程序都配一个CPU核心(你同时开了浏览器、微信、音乐播放器、编辑器......可能有几十个程序)。而且很多时间CPU其实在等待------比如等网络数据返回、等文件读完、等打字输入。如果CPU只会傻等,那大部分时间就被浪费了。并发让CPU在等待的时候去干别的事,大大提高效率。

How(怎么用)

可以用多线程或者协程来实现,后面会详细讲解。

  • 业务场景:Web服务器同时处理多个用户请求、聊天软件同时和多人聊天
  • Web后端重要性:⭐⭐⭐⭐⭐(这是Web后端的核心)
  • 面试:★★★★

面试题 :并行和并发的区别是什么?
面试答案:并行是真正的同时执行(需要多核CPU),并发是看起来像同时执行但实际上是快速切换(单核也可以)。并行关注物理上的同时,并发关注逻辑上的同时。


2. 进程、线程、协程的区别

先讲一个故事,帮助理解这三个概念:

乔布斯想开一家手机工厂。

他先建了一条生产线 ,上面有各种设备和材料。这条生产线就是进程------它包含了生产手机所需要的所有资源(材料、工具、空间),是生产活动的基础。

光有生产线不行,还得有工人 来操作。于是他招了5个工人,这些工人能真正动手把手机做出来。工人就是线程------他们是真正干活的人。

干了一段时间,乔布斯发现工人们经常要等零件送来,等的时候只能干坐着。于是他想了个招:规定工人在等待的时候,不要傻等,先去干点别的活,比如整理工具、记录数据。等零件到了再回来继续。这就是协程------工人利用等待时间去干其他事,不浪费任何时间。

2.1 进程(Process)

What(是什么)

进程就是一个正在运行的程序。你打开浏览器,就启动了一个浏览器进程;打开微信,就启动了一个微信进程。操作系统会给每个进程分配独立的内存空间,就像一个独立的"小房间",不同进程之间不能随便串门。

Why(为什么会这样设计)

如果所有程序都共享同一个内存空间,那一个程序崩溃就可能把其他程序的数据弄乱,甚至导致整个电脑崩溃。给每个进程独立的空间,就像给每个人独立的房间,互不干扰,系统就更稳定安全。

How(怎么用)

python 复制代码
import multiprocessing
import os

def say_hello(name):
    print(f"你好,我是{name}")
    print(f"我的进程ID是:{os.getpid()}")  # 每个进程有唯一的编号
    print(f"创建我的父进程ID是:{os.getppid()}")

if __name__ == '__main__':
    print(f"主进程ID:{os.getpid()}")
    # 创建子进程
    p = multiprocessing.Process(target=say_hello, args=("小明",))
    p.start()  # 启动子进程
    p.join()   # 等待子进程结束
    print("子进程结束了")
  • 特点:独立内存空间,互不影响;可利用多核CPU;但创建和切换开销大
  • 业务场景:需要隔离的任务、利用多核计算
  • Web后端重要性:⭐⭐⭐(Web服务器用多个worker进程)

2.2 线程(Thread)

What(是什么)

线程是进程内部真正干活的最小单位。一个进程可以包含多个线程,就像一条生产线上可以有多个工人。这些工人共享同一条生产线上的所有材料和工具(共享内存),但每个工人有自己的工作步骤和节奏(独立的执行路径)。

Why(为什么会设计线程)

有时候一个进程需要同时做多件事。比如微信,你一边打字聊天,一边接收消息,一边下载文件。如果只能一件事一件事做,打字的时候就不能收消息,那体验就太差了。线程让一个进程内部能同时推进多件事。

而且线程比进程更"轻量"------创建线程不需要重新分配一整套资源(生产线已经有了),切换线程比切换进程快得多,因为不需要切换整个内存空间。

How(怎么用)

python 复制代码
import threading
import time

def chat(name):
    """模拟聊天任务"""
    for i in range(3):
        print(f"{name}在聊天...第{i+1}句")
        time.sleep(1)

def download(name):
    """模拟下载任务"""
    for i in range(3):
        print(f"{name}在下载文件...{i+1}%")
        time.sleep(1)

# 创建两个线程,就像两个工人同时工作
thread1 = threading.Thread(target=chat, args=("小明",))
thread2 = threading.Thread(target=download, args=("小红",))

# 启动线程
thread1.start()
thread2.start()

# 等待两个线程完成
thread1.join()
thread2.join()

print("聊天和下载都完成了!")
  • 特点:共享进程内存;切换比进程快;但受GIL限制(后面讲)
  • 业务场景:需要同时执行多个I/O操作的场景
  • Web后端重要性:⭐⭐⭐

2.3 协程(Coroutine)

What(是什么)

协程是比线程更小的执行单位。如果说线程是工人,协程就是工人灵活的工作方式------当一个任务需要等待时,工人主动放下这个任务,去做另一个任务,等之前那个任务准备好了再切回来继续。这一切都是工人自己主动控制的,不需要领导(操作系统)来安排。

Why(为什么要发明协程)

线程虽然比进程轻,但切换线程仍然需要操作系统出面(这叫做"内核态切换"),会消耗不少时间。如果有一万个用户同时访问你的网站,难道要创建一万个线程吗?那切换的开销就太大了。

协程的切换完全在用户自己代码里完成(这叫"用户态切换"),不需要操作系统参与,非常轻量。一个线程里可以跑成千上万个协程,适合处理大量I/O等待的场景(比如Web服务器等数据库查询返回)。

How(怎么用)

python 复制代码
import asyncio

async def make_breakfast(name):
    """做早餐,每个步骤都需要等待"""
    print(f"{name}开始煮水")
    await asyncio.sleep(2)  # 等水烧开,但不傻等,去做别的事
    print(f"{name}水开了,开始煮面")
    await asyncio.sleep(2)  # 等面煮熟
    print(f"{name}面煮好了!")

async def main():
    # 同时做两份早餐
    await asyncio.gather(
        make_breakfast("妈妈"),
        make_breakfast("爸爸")
    )

asyncio.run(main())
# 总耗时约2秒,而不是4秒
  • 特点:超轻量、单线程内切换、适合I/O密集型
  • 业务场景:高并发Web API、爬虫、实时通信
  • Web后端重要性:⭐⭐⭐⭐⭐(核心技能)

三者的核心区别总结

进程 线程 协程
比喻 生产线 工人 工人灵活切换任务
能不能共享内存 ❌ 各有各的房间 ✅ 同一个房间 ✅ 同一个房间
谁来调度 操作系统 操作系统 程序员自己
切换代价 大(换房间) 中(换工具) 小(换个姿势干活)
能利用多核吗 ❌(受GIL限制) ❌(单线程内)

面试题 :进程、线程、协程的区别?
面试答案:进程是操作系统资源分配的最小单位,拥有独立内存空间;线程是CPU调度的最小单位,同一进程的线程共享内存;协程是用户态的轻量级调度单位,切换在代码层面完成。切换开销:进程 > 线程 > 协程。进程和线程由操作系统调度,协程由程序员显式控制。


3. 同步与异步

What(是什么)

想象你去书店买书:

  • 同步:你问老板有没有《Python》这本书,老板说"我查查,你等着"。于是你站在柜台前干等5分钟,期间什么也做不了。等老板查完告诉你结果,你才能继续下一步(有书就买,没书就走)。
  • 异步:你问老板有没有这本书,老板说"我查查,查到了告诉你"。你不用干等,先去旁边星巴克喝杯咖啡、刷会儿手机。15分钟后老板发微信说"有货,给你留了一本",你再去买单。

在编程中:

  • 同步:调用方发出请求后,一直等待被调用方返回结果,等到了才继续执行。过程中调用方被"阻塞"。
  • 异步:调用方发出请求后,不等待结果,继续执行自己的代码。被调用方完成后,主动通知调用方来处理结果。

Why(为什么需要异步)

如果只有同步,那每次等待(等网络回复、等文件读取、等数据库查询)都会让整个程序卡住。想象一个网站,如果每次处理用户请求都要等数据库返回,等的时候其他用户都不能访问,那这个网站就废了。

异步让程序在等待的时候去处理其他事情,一个线程就能同时应对很多请求,效率大大提升。

How(怎么用)

python 复制代码
import asyncio

# 同步方式(会阻塞)
def sync_cook():
    print("开始煮饭")
    time.sleep(3)  # 傻等3秒,什么也不干
    print("饭煮好了")

# 异步方式(不阻塞)
async def async_cook():
    print("开始煮饭")
    await asyncio.sleep(3)  # 等饭熟的这3秒可以去干别的
    print("饭煮好了")

async def async_wash():
    print("开始洗菜")
    await asyncio.sleep(2)
    print("菜洗好了")

async def main():
    # 同时煮饭和洗菜
    await asyncio.gather(async_cook(), async_wash())
    print("菜和饭都好了!")

asyncio.run(main())
# 总耗时3秒(最长的那个),而不是5秒
  • 业务场景:Web后端处理并发请求、调用第三方API
  • Web后端重要性:⭐⭐⭐⭐⭐
  • 面试:★★★★

面试题 :同步和异步的区别?
面试答案:同步是调用方等待被调用方返回结果后再继续,会阻塞当前线程;异步是调用方不等待结果立即返回,被调用方完成后通过回调、事件等方式通知调用方。异步能避免线程阻塞,提升系统吞吐量。


4. 线程 Thread

4.1 线程的创建

What(是什么)

在Python中,创建线程就像雇佣一个新工人。你可以直接把工作内容告诉他(函数包装),也可以让他按照一个标准的工人职责来做事(类包装)。

Why(为什么有两种方式)

  • 函数包装:简单直接,适合临时任务。就像临时工,告诉他干什么就行。
  • 类包装:更规范,适合复杂任务。就像正式工,有标准的工作流程(run方法),可以有自己的工具和状态。

How(怎么用)

python 复制代码
import threading
import time

# ========== 方式一:函数包装 ==========
def worker(name, count):
    """工人要干的工作"""
    for i in range(count):
        print(f"工人{name}正在干活...第{i+1}次")
        time.sleep(1)

# 创建线程(告诉工人干什么活)
t1 = threading.Thread(target=worker, args=("张三", 3))
t2 = threading.Thread(target=worker, args=("李四", 3))

# 开始工作
t1.start()
t2.start()

# 等待工人干完活
t1.join()
t2.join()
print("所有工人都干完了")

# ========== 方式二:类包装 ==========
class MyWorker(threading.Thread):
    """正式的工人类"""
    def __init__(self, name, count):
        super().__init__()  # 必须先调用父类的初始化
        self.name = name
        self.count = count

    def run(self):
        """工人标准的工作流程(必须叫run)"""
        for i in range(self.count):
            print(f"正式工{self.name}工作中...第{i+1}次")
            time.sleep(1)

# 创建正式工
w1 = MyWorker("王五", 3)
w2 = MyWorker("赵六", 3)

# 开始工作
w1.start()
w2.start()

# 等待完成
w1.join()
w2.join()
print("所有正式工都干完了")

注意

  • 必须调用 start() 才能真正启动线程,创建后不会自动运行

  • join() 是让主线程等待子线程结束

  • 业务场景:简单的后台I/O任务

  • Web后端重要性:⭐⭐

  • 面试:★★★(创建方式和start/run区别)


4.2 join():让主线程等待子线程

What(是什么)
join() 翻译过来就是"加入等待"。主线程调用子线程的 join() 方法,就相当于主线程说:"你还没干完,那我也等着,等你干完我再继续走。"

Why(为什么要这样)

默认情况下,主线程不会等子线程。如果主线程先走完了,程序就结束了,但子线程可能还没干完活就被强行打断了。这就像一个老板自己先下班了,工人活还没干完就被迫停工。

有时候我们需要等所有子线程都完成,拿到它们的结果后,主线程才能继续(比如汇总所有工人的成果)。

How(怎么用)

python 复制代码
import threading
import time

def long_task(name, seconds):
    print(f"{name}开始一个需要{seconds}秒的任务")
    time.sleep(seconds)
    print(f"{name}的任务完成了")

# 创建三个任务,完成时间不同
task_a = threading.Thread(target=long_task, args=("A任务", 3))
task_b = threading.Thread(target=long_task, args=("B任务", 1))
task_c = threading.Thread(target=long_task, args=("C任务", 2))

# 全部启动
task_a.start()
task_b.start()
task_c.start()

print("主线程: 我已经启动了所有任务")
print("主线程: 现在我等待所有任务完成...")

# 等待每一个任务完成
task_a.join()
print("主线程: A任务已经完成了")

task_b.join()
print("主线程: B任务已经完成了")

task_c.join()
print("主线程: C任务已经完成了")

print("主线程: 所有任务完成,我继续工作")
  • 业务场景:等待所有子任务完成后汇总结果
  • Web后端重要性:⭐⭐
  • 面试:★★★

4.3 守护线程(daemon thread)

What(是什么)

守护线程就像后台的清洁工。工作日白天老板和员工都在,清洁工也跟着工作;但一旦下班、所有人都走了,清洁工也得马上离开,不需要把整个大楼都打扫完。

在编程中,守护线程是:当所有"非守护线程"(包括主线程)都结束时,守护线程会被自动强制终止,不管它干没干完。

Why(为什么需要守护线程)

有些线程的工作是辅助性的(比如垃圾回收、定时保存日志、心跳检测),它们本身不是业务的核心。如果业务都做完了,这些辅助工作继续做也没有意义了。如果因为它们没完成导致程序一直不退出,反而不合适。

最典型的应用:垃圾回收器(GC)

Python的垃圾回收器会自动清理不再使用的内存。它运行在一个守护线程中。当你的程序正常运行时,GC在后台默默工作;当程序退出时,GC线程自动结束,因为剩下的内存操作系统会一并回收。

How(怎么用)

python 复制代码
import threading
import time

def background_worker():
    """一个永远干不完的后台任务"""
    count = 0
    while True:
        count += 1
        print(f"后台任务进行中...第{count}次")
        time.sleep(0.5)

def main_work():
    """主业务流程"""
    print("主业务开始")
    for i in range(3):
        print(f"主业务处理中...步骤{i+1}")
        time.sleep(1)
    print("主业务完成!")

# 创建守护线程
bg = threading.Thread(target=background_worker)
bg.daemon = True  # 设置为守护线程(必须在start之前设置)
bg.start()

# 执行主业务
main_work()

print("主程序结束,守护线程自动终止")
# 程序到这里就结束了,守护线程不会继续运行

注意区分 join() 和 daemon

  • join() → 主线程主动等待子线程结束

  • daemon → 主线程结束时,守护线程被动被终止

  • 两者解决的是不同问题,不矛盾

  • 业务场景:GC线程、心跳检测、日志刷新

  • Web后端重要性:⭐(框架底层已处理)

  • 面试:★★★

面试题 :什么是守护线程?有什么典型应用?
面试答案:守护线程是一种当所有非守护线程结束时自动终止的线程。典型应用是垃圾回收器(GC),因为程序退出时GC再回收已无意义,设为守护线程能随主程序自动结束,不会阻止进程退出。


5. GIL(全局解释器锁)

What(是什么)

GIL的全称是Global Interpreter Lock(全局解释器锁)。它是CPython解释器里的一把大锁。这把锁规定:在任何一个时刻,只有一个线程能够真正执行Python代码

也就是说,即使你创建了10个线程,即使你的电脑有8个CPU核心,同一时刻也只能有1个线程在跑Python代码,其他9个都在排队等着。

Why(为什么会有这把锁)

这要从Python的出生讲起。

1991年,Python的创始人Guido大叔开始写Python的时候,电脑基本都是单核的,根本没多核这回事。Python内部管理内存用了一种叫"引用计数"的方法------每个对象记一个数字,表示有多少地方在用这个对象。当计数变成0,对象就被回收。

问题来了:如果有多个线程同时修改这个引用计数,就可能出现计数错乱。比如线程A刚读到计数=1,准备减1;线程B也读到计数=1,也准备减1。结果两个线程都减了1,计数变成0(正确应该是-1),对象就被错误回收了,程序就崩溃了。

解决这个问题最彻底的方法是给每个对象都加锁,但那样太复杂,而且会大大降低单线程的性能。Guido大叔选择了一个简单粗暴的方案:加一把全局锁,任何时候只让一个线程执行,这样就安全了。虽然现在看这是性能瓶颈,但当时确实是最好的选择。

How(怎么应对GIL)

GIL主要影响CPU密集型 任务(需要大量计算的),对I/O密集型任务(网络请求、文件读写)影响不大。为什么呢?因为线程在等待I/O时会主动释放GIL,让其他线程有机会执行。

python 复制代码
import threading
import time

# I/O密集型任务(GIL影响小)
def io_task(name):
    print(f"{name}开始下载")
    time.sleep(2)  # 模拟I/O等待,此时会释放GIL
    print(f"{name}下载完成")

# CPU密集型任务(GIL影响大)
def cpu_task(name):
    print(f"{name}开始计算")
    total = 0
    for i in range(50_000_000):  # 大量计算
        total += i
    print(f"{name}计算完成,结果={total}")

# I/O任务用多线程------还是有效的
print("=== I/O密集型:多线程可以加速 ===")
start = time.time()
t1 = threading.Thread(target=io_task, args=("任务1",))
t2 = threading.Thread(target=io_task, args=("任务2",))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"耗时: {time.time()-start:.2f}秒")  # 约2秒,不是4秒

# CPU任务用多线程------基本没用,甚至更慢
print("\n=== CPU密集型:多线程基本无效 ===")
start = time.time()
t1 = threading.Thread(target=cpu_task, args=("任务1",))
t2 = threading.Thread(target=cpu_task, args=("任务2",))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"耗时: {time.time()-start:.2f}秒")  # 和顺序执行差不多

# 解决方案:CPU密集型用多进程
print("\n=== CPU密集型:多进程方案 ===")
import multiprocessing
start = time.time()
p1 = multiprocessing.Process(target=cpu_task, args=("进程1",))
p2 = multiprocessing.Process(target=cpu_task, args=("进程2",))
p1.start(); p2.start()
p1.join(); p2.join()
print(f"耗时: {time.time()-start:.2f}秒")  # 时间减半!
  • 业务场景:Web后端主要是I/O密集型(等数据库、等网络),GIL影响不大
  • Web后端重要性:⭐⭐⭐⭐(理解GIL才能正确选型)
  • 面试:★★★★★(必问)

面试题 :什么是GIL?它影响什么?怎么解决?
面试答案:GIL是CPython的全局解释器锁,同一时刻只允许一个线程执行Python字节码。它使得CPU密集型任务无法利用多核加速。解决方案:1) CPU密集型用多进程;2) I/O密集型用多线程或协程;3) 使用C扩展或Cython释放GIL;4) 使用无GIL的Python实现(如Jython、PyPy)。


6. 线程同步:互斥锁 Lock

What(是什么)

互斥锁就像公共厕所门上的锁。一次只能有一个人进去,其他人必须在外面排队。等里面的人出来,下一个才能进去。

在编程中,多个线程同时修改同一个数据时,可能会造成数据错乱。锁的作用就是保证同一时刻只有一个线程能操作这份数据,其他人等着。

Why(为什么需要锁)

想象你和老婆共用一个银行账户,里面有100元。你准备取80元,你老婆也准备取80元。

没有锁的情况:

  1. 你查余额:100元,够取80
  2. 你老婆也查余额:100元,够取80
  3. 你取了80,余额变成20
  4. 你老婆也取了80,余额变成-60

问题出在哪?你们同时读取了余额,都以为还有100元。这就是"竞态条件"------多个线程同时操作共享数据导致的结果不可预测。

有锁的情况:

  1. 你拿到锁,进入
  2. 你老婆也想进,但锁被人拿着,只能等
  3. 你查余额:100元,取80,余额变成20,然后释放锁
  4. 你老婆拿到锁,查余额:20元,不够取80,放弃,释放锁

这样就安全了。

How(怎么用)

python 复制代码
import threading
import time

class BankAccount:
    def __init__(self, balance, owner):
        self.balance = balance
        self.owner = owner

class WithdrawThread(threading.Thread):
    def __init__(self, amount, account, name):
        super().__init__()
        self.amount = amount
        self.account = account
        self.name = name
        self.withdrawn = 0

    def run(self):
        # 先拿到锁
        lock.acquire()

        # 检查余额是否够
        if self.account.balance >= self.amount:
            time.sleep(0.1)  # 模拟处理时间
            self.account.balance -= self.amount
            self.withdrawn = self.amount
            print(f"{self.name}取款成功:取走{self.amount}元,余额{self.account.balance}")
        else:
            print(f"{self.name}取款失败:余额不足(当前{self.account.balance},需要{self.amount})")

        # 释放锁
        lock.release()

# 创建账户和锁
account = BankAccount(100, "家庭账户")
lock = threading.Lock()

# 两人同时取钱
husband = WithdrawThread(80, account, "老公")
wife = WithdrawThread(80, account, "老婆")

husband.start()
wife.start()

husband.join()
wife.join()

print(f"最终余额:{account.balance}元")
# 输出:老公取款成功:取走80元,余额20
#       老婆取款失败:余额不足(当前20,需要80)
#       最终余额:20元 ✓

推荐用 with 语句,会自动释放锁,更安全:

python 复制代码
def run(self):
    with lock:  # 自动获取和释放锁
        if self.account.balance >= self.amount:
            self.account.balance -= self.amount
  • 业务场景:金融交易、库存扣减、计数器
  • Web后端重要性:⭐⭐(分布式系统多用数据库锁或Redis锁)
  • 面试:★★★★

7. 死锁 Deadlock

What(是什么)

死锁是两个线程互相等待对方释放锁,结果谁也走不了,永远卡住。

经典例子:两个人做饭都需要"菜刀"和"锅"。

  • 小张先拿到菜刀,等着拿锅
  • 小李先拿到锅,等着拿菜刀
  • 小张等小李放锅,小李等小张放刀→僵住了,谁也做不了饭

Why(为什么会发生)

通常是因为加锁的顺序不一致。上面的例子中,小张先拿刀后拿锅,小李先拿锅后拿刀。如果两人都约定"先拿刀再拿锅",就不会死锁。

How(怎么避免)

python 复制代码
import threading
import time

lock_knife = threading.Lock()  # 菜刀锁
lock_pot = threading.Lock()    # 锅锁

def person_a():
    """小张:统一先拿刀再拿锅"""
    with lock_knife:
        print("小张拿到了菜刀")
        time.sleep(0.1)  # 模拟切菜的时间
        with lock_pot:
            print("小张拿到了锅,可以炒菜了!")
            time.sleep(0.2)
    print("小张炒完菜,释放所有工具")

def person_b():
    """小李:也统一先拿刀再拿锅(保持顺序一致)"""
    with lock_knife:  # 关键:和person_a一样的顺序
        print("小李拿到了菜刀")
        time.sleep(0.1)
        with lock_pot:
            print("小李拿到了锅,可以炒菜了!")
            time.sleep(0.2)
    print("小李炒完菜,释放所有工具")

# 两人同时开始做饭
t_a = threading.Thread(target=person_a)
t_b = threading.Thread(target=person_b)
t_a.start()
t_b.start()
t_a.join()
t_b.join()
print("所有人都做完饭了")

预防死锁的原则

  1. 保持统一的加锁顺序
  2. 尽量避免同时持有多个锁
  3. 如果必须多个锁,可以使用 trylock(尝试获取,获取不到就放弃)
  • 业务场景:复杂资源依赖
  • Web后端重要性:⭐
  • 面试:★★

面试题 :死锁是什么?怎么避免?
面试答案:死锁是多个线程各自持有对方需要的锁,互相等待,谁也无法继续。解决方法是保持统一的加锁顺序,或避免同时持有多个锁。


8. 信号量 Semaphore

What(是什么)

信号量就像停车场入口的电子屏显示"剩余车位:3"。当有3个车位时,前3辆车可以同时停进去。第4辆车来了发现车位满了,只能等着。等有车开出来,有空位了,才能进去。

在编程中,信号量允许指定数量的线程同时访问资源,而锁只允许1个。

Why(为什么需要信号量)

有时候资源可以同时支持多个线程访问,但不能无限多。比如:

  • 数据库连接池最多10个连接,不能让100个线程都同时连
  • 文件同时读最多5个,超过可能影响性能
  • API频率限制每秒最多3个请求

用锁的话一次只能1个,太浪费;不用控制的话并发太多,系统可能扛不住。信号量正好解决这个问题。

How(怎么用)

python 复制代码
import threading
import time
import random

# 创建信号量:最多允许3个线程同时执行
semaphore = threading.Semaphore(3)

def visit_museum(name):
    """参观博物馆(最多同时容纳3人)"""
    print(f"{name}在门口排队,等待进入...")
    
    with semaphore:  # 获取"入场券",如果满了就等着
        print(f"✅ {name}进入了博物馆")
        # 参观时间随机
        visit_time = random.randint(1, 3)
        time.sleep(visit_time)
        print(f"👋 {name}参观完毕,离开博物馆(参观了{visit_time}秒)")

# 10个人同时想来博物馆
visitors = []
for i in range(1, 11):
    t = threading.Thread(target=visit_museum, args=(f"游客{i}",))
    visitors.append(t)
    t.start()

# 等待所有人参观完
for t in visitors:
    t.join()

print("博物馆闭馆了!")
  • 业务场景:连接池限流、API频率控制、爬虫并发限制
  • Web后端重要性:⭐⭐⭐
  • 面试:★

9. 事件 Event

What(是什么)

Event就像一个"信号旗"。一面是红色的(False),一面是绿色的(True)。线程可以等待旗子变绿(wait),另一个线程可以把旗子翻成绿色(set),唤醒所有等待的人。

生活中类似"同学们,菜上齐了,开吃吧!"------同学们(线程们)一直在等这句话,班长(另一个线程)喊出这句话(set),大家才开始动筷子。

Why(为什么用Event)

线程间需要协调:有些事情必须等所有线程都准备好才开始,或者等某个条件满足才触发。与其让线程不停地问"好了吗?好了吗?"(轮询,浪费CPU),不如让它们睡着等待,条件满足时主动叫醒它们。

How(怎么用)

python 复制代码
import threading
import time

# 创建事件对象(默认是False)
start_eating = threading.Event()

def guest(name):
    """客人线程:等主人喊开吃"""
    print(f"客人{name}已就座,等待开吃...")
    start_eating.wait()  # 在这里等着,直到被唤醒
    print(f"客人{name}开始吃了!")

def host():
    """主人线程:菜上齐后喊开吃"""
    print("主人:菜还没上齐,大家稍等...")
    time.sleep(3)
    print("主人:菜上齐了,大家开吃吧!")
    start_eating.set()  # 设置事件为True,唤醒所有等待的客人

# 创建4个客人
guests = []
for i in range(1, 5):
    t = threading.Thread(target=guest, args=(f"客人{i}",))
    guests.append(t)
    t.start()

# 主人也启动
host_thread = threading.Thread(target=host)
host_thread.start()

# 等待所有线程结束
for t in guests:
    t.join()
host_thread.join()

print("饭局结束!")
  • 业务场景:服务就绪通知、定时触发、游戏开始信号
  • Web后端重要性:⭐
  • 面试:★

10. 生产者与消费者模式

What(是什么)

这是一个经典的并发协作模型:

  • 生产者:负责"生产"数据的人/线程(比如厨师做包子)
  • 消费者:负责"消费"数据的人/线程(比如顾客吃包子)
  • 缓冲区:一个中转站(比如放包子的盘子)。厨师做好了放盘子里,顾客从盘子里拿。厨师不用管顾客什么时候来,顾客也不用管厨师什么时候做。

Why(为什么需要这个模式)

  1. 解耦:生产者和消费者互不依赖。换了厨师/换了顾客都不影响另一方。
  2. 平衡速度差异:厨师做包子快、顾客吃得慢?没关系,包子放盘子里。厨师做包子慢、顾客吃得快?也没关系,顾客吃完了盘子空的就等一等。
  3. 提高效率:厨师可以一直做包子,顾客可以一直吃,谁也不用等谁。

How(怎么用)

Python的 queue.Queue 就是一个现成的缓冲区,它是线程安全的(内部已经加了锁)。

python 复制代码
import threading
import queue
import time
import random

def chef(q, chef_name):
    """厨师(生产者):做包子"""
    for i in range(5):
        dumpling = f"{chef_name}做的{random.choice(['肉包','菜包','豆沙包'])}"
        print(f"👨‍🍳 {chef_name}生产了:{dumpling}")
        q.put(dumpling)
        time.sleep(random.uniform(0.5, 1))  # 做包子需要时间
    q.put(None)  # 放一个特殊的"停止"标记

def customer(q, customer_name):
    """顾客(消费者):吃包子"""
    while True:
        dumpling = q.get()  # 从盘子里拿包子
        if dumpling is None:  # 遇到停止标记
            q.put(None)  # 放回去,让其他顾客也能看到
            break
        print(f"🍽️ {customer_name}吃了:{dumpling}")
        time.sleep(random.uniform(0.3, 0.8))  # 吃包子需要时间

# 一个放包子的盘子(队列)
plate = queue.Queue()

# 1个厨师,3个顾客
chef_thread = threading.Thread(target=chef, args=(plate, "小王"))
customer1 = threading.Thread(target=customer, args=(plate, "小明"))
customer2 = threading.Thread(target=customer, args=(plate, "小红"))
customer3 = threading.Thread(target=customer, args=(plate, "小刚"))

chef_thread.start()
customer1.start()
customer2.start()
customer3.start()

chef_thread.join()
customer1.join()
customer2.join()
customer3.join()

print("\n今天的包子卖完了!")
  • 业务场景:消息队列(如Celery + Redis)、日志异步写入、爬虫调度
  • Web后端重要性:⭐⭐⭐⭐(核心设计模式)
  • 面试:★★★★

面试题 :说说生产者消费者模式,它的好处是什么?
面试答案:生产者消费者模式通过缓冲区解耦生产者和消费者。好处:1)解耦,双方互不依赖;2)平衡速度差异,缓冲区吸收波动;3)提高并发效率。Python中使用queue.Queue实现,它是线程安全的。


11. 进程 Process

11.1 进程创建

What(是什么)

前面线程是在一个程序内部创建多个"工人"。进程是更重量级的,它在操作系统中创建一个完全独立的"新程序副本"。每个进程有自己的内存空间,互不干扰。

Why(为什么需要多进程)

  1. 绕过Python的GIL锁,真正利用多核CPU做并行计算
  2. 进程之间相互隔离,一个进程崩溃不会影响其他进程
  3. 适合需要高稳定性的场景

How(怎么用)

python 复制代码
import multiprocessing
import os
import time

# ========== 方式一:方法包装 ==========
def func1(name):
    print(f"当前进程ID: {os.getpid()}")   # 当前进程的ID
    print(f"父进程ID: {os.getppid()}")    # 创建我的进程的ID
    print(f"Process:{name} start")
    time.sleep(3)
    print(f"Process:{name} end")

if __name__ == "__main__":
    print(f"主进程ID: {os.getpid()}")
    # 创建子进程
    p1 = multiprocessing.Process(target=func1, args=('进程1',))
    p2 = multiprocessing.Process(target=func1, args=('进程2',))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    print("主进程结束")

# ========== 方式二:类包装 ==========
class MyProcess(multiprocessing.Process):
    def __init__(self, name):
        super().__init__()  # 必须调用父类的初始化
        self.name = name

    def run(self):
        print(f"进程{self.name}开始了,PID={os.getpid()}")
        time.sleep(3)
        print(f"进程{self.name}结束了")

if __name__ == "__main__":
    p1 = MyProcess("工人A")
    p2 = MyProcess("工人B")
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    print("所有进程完成")

重要提醒(Windows用户特别注意)

在Windows系统上,创建进程的代码必须放在 if __name__ == "__main__": 里面,否则会无限递归创建进程,导致程序崩溃。

  • 业务场景:CPU密集型计算、Web服务器多worker
  • Web后端重要性:⭐⭐⭐
  • 面试:★★★

11.2 进程间通信 - Queue

What(是什么)

前面说了,每个进程有自己独立的内存空间,不能直接共享变量。那不同进程之间怎么传递数据呢?答案是:用一个公共的队列------就像一个收发室,进程A把信件放进收发室,进程B从收发室取走。

Why(为什么不能直接用变量)

进程间内存隔离,一个进程修改了变量,另一个进程完全看不到。这就像两个住在不同房子的人,你不能指望把你的东西放到别人家的柜子里。必须通过公共的"信箱"(队列)来传递。

How(怎么用)

python 复制代码
import multiprocessing

def worker(q):
    """子进程:从队列中接收数据"""
    print("子进程:等待接收数据...")
    data = q.get()  # 从队列获取数据
    print(f"子进程收到了:{data}")

if __name__ == '__main__':
    # 创建队列
    q = multiprocessing.Queue()

    # 必须先启动子进程
    p = multiprocessing.Process(target=worker, args=(q,))
    p.start()

    # 再放入数据(顺序很重要!)
    q.put("你好,这是主进程发来的消息")

    p.join()
    print("通信完成")

⚠️ 重要 :必须先 start 子进程,再 put 数据。如果反过来,数据可能丢失或导致卡死。因为队列内部需要两端都连接好才能正常传输。

  • 业务场景:多进程任务分发和结果收集
  • Web后端重要性:⭐⭐
  • 面试:★★

11.3 进程间通信 - Pipe

What(是什么)

Pipe就像一个两头通的管道。两个人各拿一头,说的话对方能听到(全双工),或者一个只能说一个只能听(半双工)。它比Queue更轻量,适合两个进程直接一对一通信。

Why(为什么用Pipe)

Queue适合多对多(多个生产者和多个消费者),而Pipe适合点对点(两个进程直接对话)。更轻量,更简单。

How(怎么用)

python 复制代码
import multiprocessing
import time

def func1(conn):
    """进程1:发消息给进程2,并接收回复"""
    msg = "Hello! 我是进程1"
    print(f"进程1发送: {msg}")
    conn.send(msg)
    time.sleep(0.5)
    # 接收进程2的回复
    reply = conn.recv()
    print(f"进程1收到回复: {reply}")

def func2(conn):
    """进程2:接收进程1的消息,并回复"""
    msg = conn.recv()
    print(f"进程2收到: {msg}")
    # 回复
    reply = "你好! 我是进程2,我收到你的消息了"
    print(f"进程2回复: {reply}")
    conn.send(reply)

if __name__ == '__main__':
    # 创建管道,得到两头
    conn1, conn2 = multiprocessing.Pipe()

    # 创建两个进程,各拿一头
    p1 = multiprocessing.Process(target=func1, args=(conn1,))
    p2 = multiprocessing.Process(target=func2, args=(conn2,))

    p1.start()
    p2.start()

    p1.join()
    p2.join()
    print("通信结束")
  • 业务场景:两个进程直接交换数据
  • Web后端重要性:⭐
  • 面试:★★

11.4 进程间通信 - Manager

What(是什么)

Manager就像一个"共享白板"。多个进程可以同时在这个白板上写字、查看内容。大家都看到同一块白板的内容,实现了数据的共享。

Why(为什么需要Manager)

Queue和Pipe都是"传递"数据,数据从一个进程到了另一个进程。但有时候我们需要的是"共享"数据------多个进程都能看到和修改同一份数据(像一个共享的列表、字典)。Manager就是为了这个场景设计的。

How(怎么用)

python 复制代码
import multiprocessing

def worker(shared_list, shared_dict, name):
    """子进程修改共享数据"""
    shared_list.append(f"来自{name}的数据")
    shared_dict[name] = f"{name}已完成任务"
    print(f"{name}修改了共享数据")

if __name__ == '__main__':
    # 使用 Manager 作为上下文管理器
    with multiprocessing.Manager() as manager:
        # 创建共享的列表和字典
        shared_list = manager.list(["初始数据"])
        shared_dict = manager.dict({"状态": "进行中"})

        print("修改前:", shared_list, shared_dict)

        # 创建多个进程
        p1 = multiprocessing.Process(target=worker, args=(shared_list, shared_dict, "进程A"))
        p2 = multiprocessing.Process(target=worker, args=(shared_list, shared_dict, "进程B"))
        p1.start()
        p2.start()
        p1.join()
        p2.join()

        print("修改后:", shared_list)
        print("修改后:", shared_dict)
  • 业务场景:多进程共享配置、状态
  • Web后端重要性:⭐(分布式系统更常用Redis)
  • 面试:★★

11.5 进程池 Pool

What(是什么)

进程池就像一家有固定员工数量的公司。公司不会每来一个项目就招一批新人、做完项目再辞退(那样太耗时了)。而是长期维持5个固定员工,有活就分配给他们,做完了一个接着下一个。

Why(为什么用进程池)

  1. 节省进程创建和销毁的时间(创建进程很慢)
  2. 节省内存(进程多了占内存)
  3. 控制并发数量(防止进程过多导致系统过载)

How(怎么用)

python 复制代码
import multiprocessing
import os
import time
import random

def process_task(name):
    """要执行的任务"""
    pid = os.getpid()
    print(f"进程{pid}开始处理任务: {name}")
    time.sleep(random.uniform(0.5, 2))  # 模拟处理时间
    print(f"进程{pid}完成任务: {name}")
    return f"{name}的结果"

def handle_result(result):
    """任务完成后的回调函数"""
    print(f"📬 收到结果: {result}")

if __name__ == '__main__':
    # 创建一个有3个进程的进程池
    with multiprocessing.Pool(processes=3) as pool:
        # 提交8个任务(进程只有3个,所以会自动排队)
        results = []
        tasks = ['任务A', '任务B', '任务C', '任务D', '任务E', '任务F', '任务G', '任务H']

        for task in tasks:
            # 异步提交(不等待,立刻返回)
            result = pool.apply_async(
                process_task,
                args=(task,),
                callback=handle_result  # 任务完成后自动调用
            )
            results.append(result)

        # 等待所有任务完成
        pool.close()  # 不再接受新任务
        pool.join()   # 等待所有任务结束

        print("\n所有任务都完成了!")

    # map方式:更简洁的批量处理
    print("\n=== map方式批量处理 ===")
    def double(x):
        return x * 2

    with multiprocessing.Pool(processes=4) as pool:
        results = pool.map(double, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
        print(f"结果: {results}")
  • 业务场景:批量处理数据、并行下载、图片处理
  • Web后端重要性:⭐⭐(异步任务处理)
  • 面试:★★★

12. 协程 Coroutine

12.1 yield 方式(已淘汰,仅了解)

What(是什么)

最早期的协程是用Python的生成器功能(yield)来模拟的。生成器可以在执行到一半时暂停,交出控制权,之后再从暂停的地方继续。利用这个特性可以实现任务的切换。

Why(为什么被淘汰)

yield虽然能暂停和恢复,但有个致命缺陷:它不能真正解决IO等待问题。time.sleep() 会让整个线程都睡过去,其他任务也执行不了。就像一个工人说"我等零件的时候先干别的",但实际上还是坐在那里等,并没有真正去干别的。

How(了解即可,生产不用)

python 复制代码
import time

def task1():
    """北京任务"""
    for i in range(3):
        print(f'北京:第{i}次打印啦')
        yield  # 暂停,交出控制权
        time.sleep(1)  # 这里会阻塞整个线程

def task2():
    """上海任务"""
    generator = task1()
    for k in range(3):
        print(f'上海:第{k}次打印了')
        next(generator)  # 恢复task1
        time.sleep(1)    # 这里也会阻塞

if __name__ == '__main__':
    start = time.time()
    task2()
    print(f"耗时{time.time()-start:.2f}秒")  # 约5秒(并没省时间)
  • 业务场景:无
  • Web后端重要性:⭐
  • 面试:★

12.2 asyncio 协程(现代标准,重点掌握)

What(是什么)

asyncio是Python 3.5之后正式推出的协程方案。核心是三个关键字:

  • async:声明一个函数是协程函数(可以暂停的函数)
  • await:在这里暂停,去执行其他协程,等条件满足再回来
  • asyncio.run():启动事件循环,驱动所有协程运行

事件循环(Event Loop) 就像一个大管家,管理着所有协程。哪个协程准备好了就让谁执行,遇到await就把控制权交给另一个准备好的协程。

Why(为什么asyncio这么重要)

  1. 极轻量:一个线程可以跑成千上万个协程,切换几乎无开销
  2. 非阻塞IO:遇到IO等待时,协程主动让出,事件循环去执行其他协程,CPU不浪费
  3. 简单:async/await语法清晰,就像写同步代码一样,但实际上是异步执行的

最适合的场景:Web后端(处理大量并发请求)、爬虫(同时抓取很多页面)、实时通信

How(怎么用)

python 复制代码
import asyncio
import time

async def fetch_web(url, delay):
    """模拟请求网页(每个网页返回时间不同)"""
    print(f"开始请求 {url}")
    await asyncio.sleep(delay)  # 模拟网络IO,这里不阻塞!
    print(f"{url} 请求完成(耗时{delay}秒)")
    return f"{url}的内容"

async def main():
    """主函数:同时请求多个网页"""
    start_time = time.time()

    # 同时发起3个请求
    results = await asyncio.gather(
        fetch_web("百度", 2),
        fetch_web("淘宝", 1),
        fetch_web("京东", 2)
    )

    print(f"\n所有结果: {results}")
    print(f"总耗时: {time.time() - start_time:.2f}秒")
    # 输出约2秒(最长的那个),而不是5秒

# 运行
if __name__ == '__main__':
    asyncio.run(main())

# ============ 更接近实际的例子 ============
print("\n=== 模拟Web后端处理请求 ===")

async def handle_request(user_id, request_name):
    """处理用户请求的完整流程"""
    print(f"[用户{user_id}] 收到请求: {request_name}")

    # 第1步:查询数据库(模拟)
    print(f"[用户{user_id}] 查询数据库中...")
    await asyncio.sleep(0.5)  # 模拟数据库查询耗时

    # 第2步:调用第三方API(模拟)
    print(f"[用户{user_id}] 调用外部API...")
    await asyncio.sleep(1.0)  # 模拟API调用耗时

    # 第3步:返回结果
    print(f"[用户{user_id}] 请求完成!")
    return f"[用户{user_id}] 的{request_name}结果"

async def web_server():
    """模拟Web服务器同时处理3个请求"""
    start = time.time()

    results = await asyncio.gather(
        handle_request(1, "查看订单"),
        handle_request(2, "支付"),
        handle_request(3, "查询物流")
    )

    print(f"\n处理结果: {results}")
    print(f"3个请求总耗时: {time.time() - start:.2f}秒")
    # 约1.5秒(最长的请求),而不是4.5秒

asyncio.run(web_server())
  • 业务场景:FastAPI/Starlette Web框架、爬虫、WebSocket
  • Web后端重要性:⭐⭐⭐⭐⭐(核心支柱)
  • 面试:★★★★★

面试题 :asyncio的原理是什么?和线程相比有什么优势?
面试答案:asyncio基于事件循环(Event Loop),协程通过await主动让出控制权,事件循环调度其他协程。优势:1)单线程内实现并发,无锁开销;2)协程切换在用户态,极轻量;3)一个线程可支撑上万个并发连接。线程由操作系统调度,切换开销大且受GIL限制。协程适合I/O密集型高并发场景。


13. Web后端技术选型总结

场景 推荐方案 重要度 原因
处理大量HTTP请求 FastAPI + asyncio ⭐⭐⭐⭐⭐ 异步非阻塞,单机支撑数万并发
后台CPU密集计算 进程池(multiprocessing) ⭐⭐⭐⭐ 利用多核,避开GIL
异步任务队列 Celery + Redis/RabbitMQ ⭐⭐⭐⭐ 生产者消费者模式,解耦可靠
API频率限流 Semaphore ⭐⭐⭐ 控制并发数量
文件批量处理 进程池 Pool.map ⭐⭐⭐ 并行处理大量文件
两进程通信 Pipe/Queue ⭐⭐ 简单点对点或队列通信
多进程共享状态 Manager/Redis ⭐⭐ 共享数据,分布式用Redis

📌 学习建议

作为Web后端开发者,按重要度分配学习时间:

  1. 最优先(⭐⭐⭐⭐⭐):协程asyncio、并发概念、同步/异步区别 → 这是日常工作的核心
  2. 重要(⭐⭐⭐⭐):GIL原理、生产者消费者模式 → 理解原理,指导选型
  3. 了解(⭐⭐⭐):进程/线程创建、锁、Pool → 偶尔用到,知道什么时候用就行
  4. 知道即可(⭐⭐及以下):Pipe、Manager、Event等 → 阅读代码时能看懂

💡 每个代码块都是完整可运行的,复制到 .py 文件就能直接执行。建议边看边跑代码,改改参数,感受不同。

相关推荐
代码中介商1 小时前
C语言核心知识完全回顾:从数据类型到动态内存管理
c语言·开发语言
Hello_Embed1 小时前
【无标题】
网络·笔记·网络协议·tcp/ip·嵌入式
故事还在继续吗1 小时前
C++多线程与多进程编程
开发语言·c++
幽络源小助理1 小时前
影视脚本分镜在线协作系统源码 PHP剧本创作平台
开发语言·php
测试19981 小时前
接口测试工具:Postman的高级用法
自动化测试·软件测试·python·测试工具·测试用例·接口测试·postman
小陈phd2 小时前
多模态大模型学习笔记(三十八)——传统OCR技术机制:从DBNet到CRNN:吃透传统OCR两阶段范式的底层逻辑
笔记·学习·ocr
2501_901200532 小时前
mysql数据库主键类型对性能的影响_使用自增整数优于UUID
jvm·数据库·python
.柒宇.2 小时前
FastAPI进阶教程
开发语言·python·fastapi