Python 多任务编程
Python 多任务编程
多任务概述
为什么要有多任务
在早期的单任务程序中,一个函数或方法执行完成后才能执行下一个,CPU 利用率低下。多任务可以充分利用 CPU 资源,提高程序的执行效率。例如,使用网盘同时下载多个文件,比逐个下载快得多。
什么是多任务
多任务是指在同一时间内执行多个任务。现代操作系统(如 Windows、macOS、Linux)都是多任务操作系统,可以同时运行多个软件。
多任务的两种表现形式
并发(Concurrent)
定义:在一段时间内交替去执行多个任务。
原理:对于单核 CPU,操作系统轮流让各个任务交替执行(例如任务 A 执行 0.01 秒,切换到任务 B 执行 0.01 秒,如此反复)。由于 CPU 执行速度极快,宏观上感觉所有任务都在同时执行。
本质:逻辑上同时,物理上不是真正同时。
并行(Parallel)
定义:在一段时间内真正的同时一起执行多个任务。
原理:多核 CPU 给每个内核分配一个任务,多个内核真正同时执行。
本质:物理上同时执行。
多进程(Multiprocessing)
进程的概念
进程(Process) 是操作系统资源分配的最小单位,也是一个正在运行的程序实例(如正在运行的 QQ、微信)。每个进程拥有独立的内存空间、文件描述符等资源。一个程序运行后至少有一个进程。
多进程的作用
未使用多进程:函数按顺序执行,总耗时等于各函数耗时之和。
使用多进程:多个函数可以同时执行,总耗时约等于最长的那个函数耗时(在并行情况下)。
Python 实现多进程
Python 使用 multiprocessing 模块来创建和管理进程。
基本步骤
importmultiprocessing# 1. 定义任务函数deftask():print("执行任务")# 2. 创建进程对象p=multiprocessing.Process(target=task)# 3. 启动进程p.start()给进程传递参数
args:以元组形式传递位置参数
kwargs:以字典形式传递关键字参数
defmusic(num,name):foriinrange(num):print(f"{name}听音乐...")defcoding(count):foriinrange(count):print("敲代码...")if__name__=='__main__':p1=multiprocessing.Process(target=music,args=(3,"小明"))p2=multiprocessing.Process(target=coding,kwargs={'count':3})p1.start()p2.start()获取进程编号
os.getpid():获取当前进程编号
os.getppid():获取父进程编号
importosprint(f"当前进程ID:{os.getpid()}, 父进程ID:{os.getppid()}")进程注意事项
进程间不共享全局变量
创建子进程时,会拷贝主进程的资源(包括全局变量)。每个进程拥有自己独立的全局变量副本,修改互不影响。
importmultiprocessing my_list=[]defwrite_data():foriinrange(3):my_list.append(i)print("write:",my_list)defread_data():print("read:",my_list)if__name__=='__main__':p1=multiprocessing.Process(target=write_data)p2=multiprocessing.Process(target=read_data)p1.start()p1.join()# 等待 p1 执行完毕p2.start()# 输出:write: [0,1,2] read: []- 主进程与子进程的结束顺序
默认情况下,主进程会等待所有非守护子进程结束后才退出。如果希望主进程退出时强制终止子进程,可以:
设置守护进程:p.daemon = True(必须在 start() 之前设置)
主动终止子进程:p.terminate()
p=multiprocessing.Process(target=task)p.daemon=True# 主进程结束,子进程立即销毁p.start()# 或p.terminate()# 立即终止子进程多线程(Multithreading)
线程的概念
线程(Thread)是程序执行的最小单位。一个进程至少有一个线程(称为主线程),线程共享进程的资源(内存、文件等)。创建线程的资源开销比进程小得多,适合 I/O 密集型任务。
比喻:进程像一个车间,线程是车间里的工人。工人共享车间的设备(资源),而车间之间相互独立。
Python 实现多线程
Python 使用threading模块来创建和管理线程。
基本步骤
importthreadingdeftask():print("执行任务")t=threading.Thread(target=task)t.start()传递参数(与进程类似)
defmusic(num):foriinrange(num):print("听音乐...")t=threading.Thread(target=music,args=(3,))t.start()主线程与子线程的结束顺序
默认情况下,主线程会等待所有非守护子线程结束后才退出。设置守护线程的方法:
创建时指定 daemon=True
调用 setDaemon(True) 方法
t=threading.Thread(target=task,daemon=True)t.start()线程间的执行顺序
多个线程的执行顺序由 CPU 调度决定,无序。可以通过 threading.current_thread() 获取当前线程对象。
defshow():print(threading.current_thread())foriinrange(5):t=threading.Thread(target=show)t.start()线程间共享全局变量
由于线程共享进程的资源,多个线程可以访问和修改同一份全局变量。这带来了便利,但也引发了数据竞争问题,需要使用锁(如 threading.Lock)来同步。
importthreadingdefsum1(lock:threading.Lock):# 加上threading.Lock这个变量注解以后lock.acquire()# 编辑器知道 lock 有 acquire 方法(有变量注解)foriinrange(100000):globalg_num g_num+=1print(f"sum1任务:{g_num}")lock.release()defsum2(lock:threading.Lock):# 加上threading.Lock这个变量注解以后lock.acquire()# 编辑器知道 lock 有 acquire 方法(有变量注解)foriinrange(100000):globalg_num g_num+=1print(f"sum2任务:{g_num}")lock.release()if__name__=="__main__":g_num=0lock=threading.Lock()multiple_thread_sum1=threading.Thread(target=sum1,args=(lock,))multiple_thread_sum2=threading.Thread(target=sum2,args=(lock,))multiple_thread_sum1.start()multiple_thread_sum2.start()关于 GIL(全局解释器锁)
GIL(Global Interpreter Lock,全局解释器锁) 是CPython 解释器(Python 官方实现)中的一个机制。它是一个互斥锁,保证同一时刻只有一个线程能够执行 Python 字节码。
每个 Python 进程只有一个 GIL,而不是每个 CPU 内核一个。
GIL 的存在简化了 CPython 的内存管理(特别是引用计数),避免了多线程同时操作 Python 对象时的数据竞争。
GIL 限制了多线程在 CPU 密集型任务上的并行能力。因为 GIL 强制同一进程内同一时刻只有一个线程能执行 Python 字节码。对于 CPU 密集型任务,线程几乎不主动释放 GIL,所以多个线程无法真正并行跑在多核上,只能轮流执行,加上切换开销,性能反而可能下降。
多协程(Coroutine)
生成器(Generator)基础
协程是从生成器发展而来的。生成器是一种可以逐步产生值的函数,使用 yield 关键字。
生成器推导式
gen=(i*2foriinrange(5))# 生成器对象print(next(gen))# 0print(next(gen))# 2yield 生成器函数
defgenerator(n):foriinrange(n):print("生成前")yieldi# 暂停并返回 iprint("生成后")g=generator(3)print(next(g))# 生成前 → 0print(next(g))# 生成后 → 生成前 → 1yield 会暂停函数,并返回一个值;下次调用 next() 时,从暂停处继续执行。
生成器产生完所有值后,再次调用 next() 会抛出 StopIteration 异常。
协程的概念
协程(Coroutine) 是一种用户态的轻量级线程,由程序员控制切换点,而不是由操作系统抢占。Python 从 3.5 开始使用 async/await 语法原生支持协程。
主要目的:执行异步任务(如网络请求、文件读写)。
控制流:双向(调用者 ↔ 协程),协程可以主动让出控制权。
调度:由事件循环(如 asyncio)统一调度。
协程三要素
函数定义前加 async → 定义协程函数
等待异步操作时加 await → 挂起点,主动让出控制权
使用 asyncio.run() 启动 → 创建事件循环并运行协程
importasyncioasyncdefhello(name):print(f"开始:{name}")awaitasyncio.sleep(1)# 模拟 I/O 操作,主动让出 CPUprint(f"结束:{name}")asyncdefmain():# 并发执行两个协程task1=asyncio.create_task(hello("Alice"))task2=asyncio.create_task(hello("Bob"))awaittask1awaittask2 asyncio.run(main())await 后面必须跟一个 awaitable 对象(如协程、asyncio.Future、asyncio.Task)。
asyncio.create_task() 将协程包装为任务,使其并发执行。
协程的切换发生在 await 处,且由程序员显式控制,因此没有线程切换的开销。
create_task 作用
asyncio.create_task() 用于并发执行多个协程。它将一个协程包装成 Task 对象,并安排到事件循环中执行,从而实现多任务并发
asyncdefmain():task1=asyncio.create_task(hello())task2=asyncio.create_task(hello())awaittask1awaittask2没有 create_task:只能顺序执行协程(一个完成后才执行下一个)。
有 create_task:多个协程可以交替运行(并发)。
协程 vs 线程 vs 进程对比
| 对比项 | 协程 | 线程 | 进程 |
|---|---|---|---|
| 创建数量 | 轻松上万 | 最多几百 | 最多几十 |
| 适用场景 | I/O 密集型(网络、文件) | I/O 密集型 | CPU 密集型 |
| 内存占用 | 很小(几 KB) | 较大(几 MB) | 很大(几十 MB) |
| 数据共享 | 直接共享(无需加锁) | 小心共享(需要加锁) | 不能直接共享(需 IPC (管道,消息队列,共享内存等)) |
| 切换成本 | 极低(用户态) | 中等(内核态) | 高(内核态 + 上下文切换) |
| 利用多核 | 否(单线程内) | 受 GIL 限制 | 是(独立进程) |
| 一句话总结 | 单线程内切换做事 | 看起来同时做事 | 真正同时做事 |
应用场景选择指南
if主要是网络请求or文件读写:# I/O 密集型用协程# 最佳选择,性能最高elif主要是数学计算:# CPU 密集型用多进程# 绕过 GIL,利用多核else:# 简单的后台任务用多线程# 简单易用简单比喻
协程:单线程魔术师,手里抛接多个球(I/O 等待时换件事做)。
线程:多个魔术师,但只有一个能同时表演(GIL 限制)。
进程:多个魔术师,各自独立表演(完全独立)。
综合对比示例
以下示例演示了同步、多线程、协程三种方式执行两个耗时 1 秒的 I/O 任务的耗时对比:
importtimeimportthreadingimportasynciodefmock_io(delay,name):time.sleep(delay)returnf"{name}完成"defsync_version():start=time.time()mock_io(1,"任务1")mock_io(1,"任务2")print(f"同步:{time.time()-start:.1f}秒")# 约 2 秒defthread_version():start=time.time()t1=threading.Thread(target=mock_io,args=(1,"线程1"))t2=threading.Thread(target=mock_io,args=(1,"线程2"))t1.start()t2.start()t1.join()t2.join()print(f"线程:{time.time()-start:.1f}秒")# 约 1 秒asyncdefasync_version():start=time.time()asyncdefasync_io(delay,name):awaitasyncio.sleep(delay)returnf"{name}完成"task1=asyncio.create_task(async_io(1,"协程1"))task2=asyncio.create_task(async_io(1,"协程2"))awaittask1awaittask2print(f"协程:{time.time()-start:.1f}秒")# 约 1 秒sync_version()thread_version()asyncio.run(async_version())总结
| 技术 | 资源开销 | 数据共享 | 适用场景 | 核心优势 |
|---|---|---|---|---|
| 多进程 | 高 | 不共享(需 IPC) | CPU 密集型 | 利用多核,绕过 GIL |
| 多线程 | 中 | 共享(需加锁) | I/O 密集型 | 简单易用,资源比进程省 |
| 多协程 | 极低 | 共享(无需加锁) | 高并发 I/O | 极低切换成本,支持海量并发 |
选型建议:
计算密集 → 多进程
网络爬虫、Web 服务器 → 协程
简单的后台任务 → 多线程
