Python进阶 多线程、生成器与协程
一. 多线程入门
1. 重点进程、线程与主线程
- 进程 :CPU资源分配的最小单位;创建一个进程就会分得一定资源。
- 线程 :CPU 调度 的基本单位,依附于进程 执行代码;每个进程至少有一个线程 ,通常就是主线程。
- 多任务 :在 Python 里除多进程 外,还可以在同一进程 里用多线程完成多任务;写法和多进程很像,主要是模块名、类名不同。
2. 多线程三步走
-
导入线程模块 :
import threading -
创建线程对象 :
线程对象 = threading.Thread(target=任务函数名)(还可传args、kwargs、name等) -
启动线程 :
线程对象.start() -
target=写函数名 ,不要写成函数()。 -
对比多进程 :
multiprocessing.Process换成threading.Thread,Process(...)换成Thread(...),同样是target、args、kwargs、start()。 -
if __name__ == '__main__'::多进程在 Windows 上必须 这样写,防止子进程重复加载模块;多线程一般不强制 ,但建议也写上,结构清楚。
3. Thread 参数与线程名
args:元组 按位置传参,顺序要和任务函数的形参顺序 一致;只有一个参数时写成(x,)。kwargs:字典 按关键字传参,字典的 key 必须和任务函数的形参名字一致(与名字对应,不是"猜顺序")。name:线程名,可不写;默认如Thread-1、Thread-2。- 当前线程 :
threading.current_thread();名字 :threading.current_thread().name(主线程常见为MainThread)。 - 线程编号 :
threading.current_thread().ident(解释器内编号)、threading.current_thread().native_id(操作系统线程编号),需要时使用。
4. 线程执行顺序
- 多个线程谁先执行、谁先打印 由 CPU 调度 决定,没有固定顺序;多次运行顺序可能不同,属于正常现象。
- 进程 之间被谁调度执行同样具有无序性(操作系统调度)。
- 线程需要先后关系 时,可用
join()、互斥锁或线程同步 等手段配合,不能默认"先start就先执行完"。
二. 共享全局变量与守护线程
1. 线程之间共享全局变量
- 同进程内的线程共享同一块内存 ,因此可以共享全局变量 ;进程之间不共享全局变量 (各是各的内存),这是进程和线程的核心区别之一。
- 验证思路 :多个子线程里打印进程号 (如
os.getpid()或multiprocessing.current_process().pid),相同说明同一进程;一个线程写列表、另一个线程读列表,能读到说明共享。 global:对全局变量 做原地修改(如my_list.append)常不必 写global;对全局变量重新赋值 (如给不可变类型count做count = count + 1)要在函数里写global count。
2. 主线程等待与守护线程
- 默认 :主线程会等待所有子线程执行结束后,程序才结束(子线程还在跑时,进程往往不会提前退出)。
- 守护线程 :
- 创建时传入:
threading.Thread(target=..., daemon=True) - 或:先创建线程对象,在
start()之前 设置线程对象.daemon = True - 另有一种
setDaemon(True)方法,属于旧写法,能见到即可,优先用上面两种。
- 创建时传入:
- 线程同步 / 数据安全 问题见下一章;进程 里还有
terminate()等,线程 侧一般不那样结束,主要靠守护或业务上自行结束。
三. 线程安全、互斥锁与 GIL
1. 共享全局变量带来的数据错误
- 两个线程都对同一全局变量做大量
+1时,结果可能远小于 理论值(例如各加 100 万次却得不到 200 万),因为多线程同时读写 时,读-改-写 可能被拆开交错执行,造成丢更新。 - PPT 说明 :解释器较新版本上有时现象不如** Python 3.8** 等设备上明显,课堂演示可能指定版本观察。
2. 线程同步与互斥锁
-
线程同步 :让多个线程按约定先后 访问共享资源,避免乱抢。互斥锁 是常用手段:同一时刻只有一个线程能进入"上锁"的那段代码。
-
互斥锁 :对共享数据 加锁,多个线程一起抢锁 ,抢到的先执行,其它等待 ;用完后释放锁,其它线程再抢。
-
使用流程 :
threading.Lock()创建锁 →acquire()上锁 → 操作共享变量 →release()释放锁。也可用with lock:,离开代码块时自动释放,减少忘记 release 导致死锁。
pythonlock = threading.Lock() def task(): with lock: # 同一时间只有一个线程能执行这里 pass
- 多线程必须用同一个锁对象 (同一个
lock传入或共用一个全局锁),每个人Lock()一把新锁等于没互斥。
3. 死锁
- 含义 :线程一直等对方释放锁 ,程序卡住、无法继续。
- 常见原因 :上锁后没有在合适地方释放、锁嵌套不当等。
4. join 与互斥锁
- PPT 结论 :线程共享全局变量出现竞争时,除互斥锁外,
join()也是一种思路,本质容易把并行拉成串行,简单但可能牺牲效率。 - 锁的粒度 :锁包住整段循环 时更接近单任务;锁在循环内部 时仍可能看到线程交替 ,但最终以共享数据是否正确为准。
5. GIL 与 threading.Lock
- GIL(Global Interpreter Lock) :CPython 里隐式 的全局互斥,同一时刻通常只有一条线程在执行 Python 字节码 ;多核上纯 Python 计算密集型 任务往往难以真正多核并行。
threading.Lock:你自己创建的显式 互斥锁,保护你的共享变量。- 为什么有 GIL 还要 Lock? 一行源码可能对应多条字节码 ,字节码之间仍可能切换线程,业务上的"读-改-写"仍会被打断,所以该加锁还要加锁。
- PPT 补充 :GIL 来自早期 CPython 设计(降低开销与死锁风险等),当时多核不普及;如今生态依赖多,CPython 仍保留 GIL ,多进程、**异步(async)**等是常见补充方案。
- 运用直觉 :I/O 多 多考虑线程/协程 ;CPU 算力 要甩开 GIL 常考虑多进程 等(在 Python / CPython 前提下讨论)。
四. 进程与线程的对比
1. 关系
- 线程依附在进程里,没有进程就没有线程。
- 一个进程默认一条主线程 ,还可以再创建多个子线程。
2. 区别与优缺点(Python 语境)
- 全局变量 :进程之间不共享 ;线程之间共享 ,但要注意资源竞争 ,解决办法包括
join()和 互斥锁。 - 开销 :创建进程 > 创建线程。
- 单位 :进程是操作系统资源分配 的基本单位;线程是 CPU 调度的基本单位。
- 独立执行 :线程不能脱离进程单独跑。
- 稳定性(大纲表述) :多进程 开发往往比单进程里只开多线程 更稳一些------某个进程出问题,不一定会拖垮其它进程。
- 优缺点小结 :
- 进程 :能用多核 、更接近真正并行 ;缺点是资源开销大。
- 线程 :开销小 ;在 Python 里受 GIL 等影响,不能指望用多线程把 CPU 密集型算满多核 ,更多是并发协作。
多进程,多线程与协程的区别**
五. 生成器与 yield
1. 什么是生成器
- 按程序员定的规则 一次次生成数据,条件不成立就结束 ;数据不是一次性全部进内存 ,而是用一个再生成一个 ,省内存。
- 创建方式 :① 生成器推导式 ② 含
yield的函数(生成器函数)。
2. 生成器推导式
- 与列表推导式类似,但用小括号
();括号表示生成器 ,里面是生成规则 ,得到的是生成器对象,不是立刻算好的整表。 - 取值:
next(生成器)每次取下一个;for 变量 in 生成器:遍历剩余全部。
pythonmy_generator = (i * 2 for i in range(5)) for value in my_generator: print(value)
3. yield 生成器函数
- 只要在
def里出现yield,就是生成器函数 ;调用函数得到生成器,不会一口气跑完函数体。 - 执行到
yield:暂停 ,把后面的值返回;下次next或for从暂停处往下执行。 - 生成器耗尽 后再
next,会抛出StopIteration。for循环会自动处理 这个结束;若用while+next(),一般要自己处理异常 或改用for。 return:结束生成器;具体与StopIteration的关系进阶再细讲,基础阶段掌握"结束迭代"即可。
pythondef mygenerater(n): for i in range(n): print('开始生成...') yield i print('完成一次...')
六. 协程
1. 与生成器的关系
- Python 协程是从生成器 发展而来的:早期有
yield,后来有yield from,Python 3.5 起有async/await,底层仍与可暂停的执行有关。 - 一句话 :协程让 I/O 等待 的时候不闲着,去干别的事(协作式并发)。
- 协程运行在线程里 ,由
asyncio事件循环调度。
2. 协程三要素
- 函数前加
async(async def定义协程函数;调用得到协程对象,不会立刻从头执行完)。 - 等待 处加
await(等待可 await 的对象,如asyncio.sleep、另一个协程等;在等待点把控制权交回事件循环)。 - 顶层入口用
asyncio.run(...)启动(最常用写法)。
3. 常见书写顺序
async def声明协程函数。- 需要并发 多个协程时,用
asyncio.create_task(...)包装成任务。 - 用
await等待协程或任务结束。 - 程序入口
asyncio.run(main())。
- 注意 :对同一个协程函数连写两次
asyncio.run(...),两次之间是串行 的,时间相加;要并发 ,应在一个async def里create_task多个任务 再await。
pythonimport asyncio async def hello(name): print(f"开始: {name}") await asyncio.sleep(1) print(f"结束: {name}") async def main(): await hello("Alice") print("---") task1 = asyncio.create_task(hello("Bob")) task2 = asyncio.create_task(hello("Charlie")) await task1 await task2 asyncio.run(main())
4. 协程、线程、进程
- 进程 :资源隔离、可多核并行算力,开销大。
- 线程 :共享进程内内存,适合不少 I/O 场景;CPython 下 CPU 纯算往往不靠多线程撑满多核。
- 协程 :单线程内 用事件循环调度大量 I/O 等待 任务,不开很多系统线程 也能高并发;不替代 多进程做重度 CPU 并行。
总结
1. 互动
-
创建线程的三个步骤是什么?
答 :
import threading→threading.Thread(target=任务函数名, ...)→start();target写函数名,不要写成函数()。 -
线程执行顺序为什么可能每次不一样?
答 :由 CPU / 操作系统调度 决定,没有固定先后顺序;多次运行顺序可能不同,属正常现象。
-
主线程 默认会怎么对待子线程?守护线程的作用是什么?有哪两种常见设置方式?
答 :默认 主线程会等所有子线程结束 ,进程才结束。守护线程:主线程退出后,子线程不再继续 。常见设置:
Thread(..., daemon=True);或在start()之前线程对象.daemon = True。 -
线程之间能不能共享全局变量?与进程有何不同?
答 :同进程内线程可以 共享全局变量;进程之间各自内存,不共享 全局变量。对全局变量重新赋值 往往要写
global。 -
线程同步 要解决什么问题?互斥锁 的大致步骤是什么?死锁是什么?
答 :线程同步 :约定先后访问共享资源,避免乱抢。互斥锁 :
threading.Lock()→acquire()/with lock:→ 操作共享数据 →release()(with可自动释放)。死锁:线程互相等对方释放锁,程序卡死。 -
GIL 和
threading.Lock分别指什么?为什么有两层锁的说法?答 :GIL :CPython 里解释器层的全局锁,同一时刻通常只有一条线程执行 Python 字节码 。Lock :保护你自己共享数据的显式 锁。为什么还要 Lock:一行源码对应多条字节码,线程仍可能在「读---改---写」中间被切换,业务数据仍要加锁。
-
进程和线程在共享变量、开销、多核、稳定性方面怎样对比?
答 :进程:不共享 全局变量、创建开销大 、更易 用满多核 做并行。线程:共享 进程内内存、开销较小 ;在 Python 里受 GIL 影响,CPU 密集型难靠多线程吃满多核,多偏向 I/O 并发。稳定性方面,大纲上常写多进程相对更「稳」一些。
-
生成器 的两种创建方式?
yield和return在生成器函数里区别?答 :① 生成器推导式 (小括号) ②
def里含yield的生成器函数 。yield:暂停并产出值 ,下次从暂停处继续;return:结束生成器 (迭代结束与StopIteration的关系进阶再细讲)。 -
协程三要素 是什么?
asyncio.run与create_task分别什么时候用?答 :①
async def②await③ 入口常用asyncio.run(...)。asyncio.run:程序最外层 启动事件循环、跑顶层协程。create_task:在同一个async def里把多个协程包装成任务 以便 并发 ,再配合await等待结束。对同一协程连写两次asyncio.run是串行 、时间相加;要并发应在一个 协程里create_task多个再await。