当前位置: 首页 > news >正文

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))# 2

yield 生成器函数

defgenerator(n):foriinrange(n):print("生成前")yieldi# 暂停并返回 iprint("生成后")g=generator(3)print(next(g))# 生成前 → 0print(next(g))# 生成后 → 生成前 → 1
  • yield 会暂停函数,并返回一个值;下次调用 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 服务器 → 协程

  • 简单的后台任务 → 多线程

http://www.cnnetsun.cn/news/1758181.html

相关文章:

  • 如何深度调试AMD Ryzen系统:SMUDebugTool完整指南与故障排除
  • 英雄联盟LCU API自动化工具:League-Toolkit专业配置与实战指南
  • 突破VMware macOS限制:Auto-Unlocker的完整解决方案
  • 从零到高手:DouZero AI斗地主助手完整使用指南
  • 喜马拉雅音频高效管理工具:全平台适配的批量下载解决方案
  • 2026.4.7本地初次跑灵衍 生图代码 若干问题
  • Vue3+Vite+TypeScript+ElementPlus项目最优配置
  • 3分钟极速掌控Adobe全系列:GenP 3.0全功能解锁工具深度指南
  • c#字符串函数
  • 开源PLC工具:工业控制编程零基础入门实战指南
  • YOLOv12跨平台开发指南:Python、C++、Rust多语言实现终极教程
  • Dwarf433库详解:433MHz任意波形发射与ASK/OOK信号克隆
  • OpenClaw技能商店精选:Qwen3-32B-Chat镜像加持的5个效率工具
  • 零基础玩转OpenClaw:用SecGPT-14B自动分析Wireshark日志
  • SpringBoot+Vue 中小企业设备管理系统管理平台源码【适合毕设/课设/学习】Java+MySQL
  • CosyVoice3实战案例:3秒录音生成四川话配音,效果惊艳
  • 通义千问1.5-1.8B-Chat-GPTQ-Int4在计算机网络教学中的应用
  • CefFlashBrowser:如何在现代系统上安全运行Flash内容的专业解决方案
  • 整数拼接(参照acwing的yxc)
  • 从零开始:使用SDKManager为Jetson Xavier NX刷机(含JetPack 4.6配置)
  • 个人博客上线必备:从ICP备案到公安备案的全流程保姆级教程(2024最新)
  • 别再乱调接口了!企微自建应用获取成员手机号、邮箱的最新正确姿势(2023年8月后)
  • Filter Solutions保姆级教程:从幅频响应调试到MATLAB联合仿真
  • XGBoost特征重要性全解析:weight/gain/cover三种计算方式有什么区别?
  • 鸿蒙HarmonyOS无线调试实战:摆脱数据线束缚的DS配置指南
  • 保姆级教程:用DBeaver 23.3连接人大金仓KingbaseES V8R3,附驱动下载与常见连接失败排查
  • 6.s081 Lab:从零构建一个简易的xv6 Shell
  • Ansys Q3D电容提取实战:从平行板到MEMS芯片的完整流程(附避坑指南)
  • 用好AI大纲,让写作思路更清晰
  • OpenClaw多模型切换指南:Gemma-3-12b-it与Qwen混合调用策略