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. 创建进程对象pmultiprocessing.Process(targettask)# 3. 启动进程p.start()给进程传递参数args以元组形式传递位置参数kwargs以字典形式传递关键字参数defmusic(num,name):foriinrange(num):print(f{name}听音乐...)defcoding(count):foriinrange(count):print(敲代码...)if__name____main__:p1multiprocessing.Process(targetmusic,args(3,小明))p2multiprocessing.Process(targetcoding,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__:p1multiprocessing.Process(targetwrite_data)p2multiprocessing.Process(targetread_data)p1.start()p1.join()# 等待 p1 执行完毕p2.start()# 输出write: [0,1,2] read: []主进程与子进程的结束顺序默认情况下主进程会等待所有非守护子进程结束后才退出。如果希望主进程退出时强制终止子进程可以设置守护进程p.daemon True必须在 start() 之前设置主动终止子进程p.terminate()pmultiprocessing.Process(targettask)p.daemonTrue# 主进程结束子进程立即销毁p.start()# 或p.terminate()# 立即终止子进程多线程Multithreading线程的概念线程Thread是程序执行的最小单位。一个进程至少有一个线程称为主线程线程共享进程的资源内存、文件等。创建线程的资源开销比进程小得多适合 I/O 密集型任务。比喻进程像一个车间线程是车间里的工人。工人共享车间的设备资源而车间之间相互独立。Python 实现多线程Python 使用threading模块来创建和管理线程。基本步骤importthreadingdeftask():print(执行任务)tthreading.Thread(targettask)t.start()传递参数与进程类似defmusic(num):foriinrange(num):print(听音乐...)tthreading.Thread(targetmusic,args(3,))t.start()主线程与子线程的结束顺序默认情况下主线程会等待所有非守护子线程结束后才退出。设置守护线程的方法创建时指定 daemonTrue调用 setDaemon(True) 方法tthreading.Thread(targettask,daemonTrue)t.start()线程间的执行顺序多个线程的执行顺序由 CPU 调度决定无序。可以通过 threading.current_thread() 获取当前线程对象。defshow():print(threading.current_thread())foriinrange(5):tthreading.Thread(targetshow)t.start()线程间共享全局变量由于线程共享进程的资源多个线程可以访问和修改同一份全局变量。这带来了便利但也引发了数据竞争问题需要使用锁如 threading.Lock来同步。importthreadingdefsum1(lock:threading.Lock):# 加上threading.Lock这个变量注解以后lock.acquire()# 编辑器知道 lock 有 acquire 方法(有变量注解)foriinrange(100000):globalg_num g_num1print(fsum1任务:{g_num})lock.release()defsum2(lock:threading.Lock):# 加上threading.Lock这个变量注解以后lock.acquire()# 编辑器知道 lock 有 acquire 方法(有变量注解)foriinrange(100000):globalg_num g_num1print(fsum2任务:{g_num})lock.release()if__name____main__:g_num0lockthreading.Lock()multiple_thread_sum1threading.Thread(targetsum1,args(lock,))multiple_thread_sum2threading.Thread(targetsum2,args(lock,))multiple_thread_sum1.start()multiple_thread_sum2.start()关于 GIL全局解释器锁GILGlobal 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(生成后)ggenerator(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():# 并发执行两个协程task1asyncio.create_task(hello(Alice))task2asyncio.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():task1asyncio.create_task(hello())task2asyncio.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():starttime.time()mock_io(1,任务1)mock_io(1,任务2)print(f同步:{time.time()-start:.1f}秒)# 约 2 秒defthread_version():starttime.time()t1threading.Thread(targetmock_io,args(1,线程1))t2threading.Thread(targetmock_io,args(1,线程2))t1.start()t2.start()t1.join()t2.join()print(f线程:{time.time()-start:.1f}秒)# 约 1 秒asyncdefasync_version():starttime.time()asyncdefasync_io(delay,name):awaitasyncio.sleep(delay)returnf{name}完成task1asyncio.create_task(async_io(1,协程1))task2asyncio.create_task(async_io(1,协程2))awaittask1awaittask2print(f协程:{time.time()-start:.1f}秒)# 约 1 秒sync_version()thread_version()asyncio.run(async_version())总结技术资源开销数据共享适用场景核心优势多进程高不共享需 IPCCPU 密集型利用多核绕过 GIL多线程中共享需加锁I/O 密集型简单易用资源比进程省多协程极低共享无需加锁高并发 I/O极低切换成本支持海量并发选型建议计算密集 → 多进程网络爬虫、Web 服务器 → 协程简单的后台任务 → 多线程