在现代软件开发中,充分利用计算机资源、提高程序执行效率是每个开发者追求的目标。Python 作为一门流行的编程语言,提供了多种并发编程方案,包括进程、线程和协程。本文将详细介绍这三种并发机制的概念、特点、使用方法以及适用场景,帮助读者理解并掌握 Python 并发编程的核心技术
一、多任务的概念
多任务是指在同一时间内执行多个任务的能力。在日常生活中,我们经常同时做几件事情,比如一边听音乐一边写代码,这就是多任务的体现。在计算机领域,多任务同样重要,它可以:
-
提高程序执行效率
-
充分利用 CPU 资源
-
提升用户体验
在 Python 中,实现多任务主要有三种方式:多进程、多线程和协程
二、进程的概念与使用
2.1 什么是进程
进程(Process) 是操作系统进行资源分配和调度运行的基本单位。通俗地理解,一个正在运行的程序就是一个进程。例如:
- 正在运行的 QQ
- 打开的浏览器
- 运行中的 Python 脚本
重要特性:
- 进程是资源分配的最小单位
- 一个程序运行后至少有一个进程
- 每个进程拥有独立的内存空间
2.2 多进程的优势
使用多进程可以实现真正的并行执行,充分利用多核 CPU 的优势:
- 未使用多进程:程序默认创建一个主进程,所有任务在这个进程中串行执行
- 使用多进程:主进程可以创建多个子进程,多个任务并行执行
2.3 Python 中的多进程实现
Python 提供了 multiprocessing 模块来创建和管理进程。
基本使用步骤:
- 导入模块:import multiprocessing
- 创建进程对象:process = multiprocessing.Process (target=任务函数)
- 启动进程:process.start ()
代码示例:
import multiprocessing
import time
def music():
"""播放音乐的任务"""
for i in range(3):
print("正在播放音乐...")
time.sleep(1)
def coding():
"""编写代码的任务"""
for i in range(3):
print("正在编写代码...")
time.sleep(1)
if __name__ == "__main__":
# 创建音乐进程
music_process = multiprocessing.Process(target=music)
# 创建编码进程
coding_process = multiprocessing.Process(target=coding)
# 启动进程
music_process.start()
coding_process.start()
print("主进程继续执行...")
2.4 进程的参数传递
进程可以执行带有参数的任务,支持位置参数和关键字参数:
def music(singer, times):
"""播放指定歌手的音乐多次"""
for i in range(times):
print(f"正在播放{歌手}的音乐...")
time.sleep(1)
def coding(language, lines):
"""使用指定语言编写代码"""
for i in range(lines):
print(f"使用{language}编写第{i+1}行代码...")
time.sleep(1)
if __name__ == "__main__":
# 使用args传递位置参数
music_process = multiprocessing.Process(target=music, args=("周杰伦", 3))
# 使用kwargs传递关键字参数
coding_process = multiprocessing.Process(target=coding, kwargs={"language": "Python", "lines": 5})
music_process.start()
coding_process.start()
2.5 进程编号管理
每个进程都有唯一的编号,便于进程管理:
- pid:当前进程编号
- ppid:父进程编号
import os
import multiprocessing
def task():
"""获取进程信息的任务"""
print(f"子进程:pid={os.getpid()}, ppid={os.getppid()}")
if __name__ == "__main__":
print(f"主进程:pid={os.getpid()}")
process = multiprocessing.Process(target=task)
process.start()
process.join() # 等待子进程完成
2.6 进程应用注意事项
(1) 进程间不共享全局变量
这是进程最重要的特性之一。每个子进程都会拷贝主进程的资源,因此:
import multiprocessing
import time
my_list = []
def write_data():
"""向列表中写入数据"""
for i in range(5):
my_list.append(i)
time.sleep(0.1)
print(f"写入完成后的列表:{my_list}")
def read_data():
"""读取列表中的数据"""
time.sleep(1) # 等待写入完成
print(f"读取到的列表:{my_list}")
if __name__ == "__main__":
write_process = multiprocessing.Process(target=write_data)
read_process = multiprocessing.Process(target=read_data)
write_process.start()
read_process.start()
write_process.join()
read_process.join()
print(f"主进程中的列表:{my_list}")
运行结果分析:
- 写入进程的列表:[0, 1, 2, 3, 4]
- 读取进程的列表:[](空列表)
- 主进程的列表:[](空列表)
这说明每个进程操作的都是自己独立的变量副本
(2) 主进程与子进程的结束顺序
默认情况下,主进程会等待所有子进程完成后再结束。如果希望主进程结束时子进程也自动结束,可以设置守护进程:
import multiprocessing
import time
def long_running_task():
"""长时间运行的任务"""
for i in range(10):
print(f"子进程运行中... {i}")
time.sleep(1)
if __name__ == "__main__":
# 创建守护进程
process = multiprocessing.Process(target=long_running_task)
process.daemon = True # 设置为守护进程
process.start()
print("主进程执行完毕,即将退出...")
time.sleep(3) # 主进程等待3秒后退出
三、线程的概念与使用
3.1 什么是线程
线程(Thread) 是程序执行的最小单位,它是进程中的一个实体。线程自己不拥有系统资源,只需要一点儿在运行中必不可少的资源,但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。
形象比喻:
- 进程就像一个 QQ 软件
- 线程就像 QQ 中的聊天窗口
- 一个 QQ(进程)可以打开多个聊天窗口(线程)
3.2 为什么使用多线程
相比多进程,多线程具有以下优势:
- 资源开销小:创建线程比创建进程消耗的资源少
- 启动速度快:线程启动比进程启动更快
- 通信简单:同一进程内的线程共享内存空间
3.3 Python 中的多线程实现
Python 提供了threading模块来创建和管理线程。
基本使用步骤:
- 导入模块:import threading
- 创建线程对象:thread = threading.Thread(target=任务函数)
- 启动线程:thread.start()
代码示例:
import threading
import time
def music():
"""播放音乐的任务"""
for i in range(3):
print("正在播放音乐...")
time.sleep(1)
def coding():
"""编写代码的任务"""
for i in range(3):
print("正在编写代码...")
time.sleep(1)
if __name__ == "__main__":
# 创建线程
music_thread = threading.Thread(target=music)
coding_thread = threading.Thread(target=coding)
# 启动线程
music_thread.start()
coding_thread.start()
print("主线程继续执行...")
3.4 线程的参数传递
与进程类似,线程也支持参数传递:
import threading
import time
def music(singer, times):
"""播放指定歌手的音乐"""
for i in range(times):
print(f"正在播放{歌手}的音乐...")
time.sleep(1)
def coding(language, lines):
"""使用指定语言编写代码"""
for i in range(lines):
print(f"使用{language}编写代码...")
time.sleep(1)
if __name__ == "__main__":
# 创建带参数的线程
music_thread = threading.Thread(target=music, args=("周杰伦", 3))
coding_thread = threading.Thread(target=coding, kwargs={"language": "Python", "lines": 5})
music_thread.start()
coding_thread.start()
3.5 线程的执行顺序
线程的执行顺序是不确定的,由 CPU 调度决定:
import threading
import time
def task(thread_num):
"""线程执行的任务"""
time.sleep(0.5) # 模拟任务执行时间
print(f"线程{thread_num}执行完成")
if __name__ == "__main__":
print("开始创建线程...")
# 创建10个线程
threads = []
for i in range(10):
thread = threading.Thread(target=task, args=(i,))
threads.append(thread)
thread.start()
# 等待所有线程完成
for thread in threads:
thread.join()
print("所有线程执行完成")
运行结果特点:
- 线程的执行顺序是随机的
- 每次运行的结果可能不同
- 这体现了线程调度的不确定性
3.6 线程间共享全局变量
这是线程与进程的重要区别。同一进程内的线程共享全局变量:
import threading
import time
my_list = []
def write_data():
"""向列表中写入数据"""
for i in range(5):
my_list.append(i)
time.sleep(0.1)
print(f"写入线程的列表:{my_list}")
def read_data():
"""读取列表中的数据"""
time.sleep(1) # 等待写入完成
print(f"读取线程的列表:{my_list}")
if __name__ == "__main__":
write_thread = threading.Thread(target=write_data)
read_thread = threading.Thread(target=read_data)
write_thread.start()
read_thread.start()
write_thread.join()
read_thread.join()
print(f"主线程中的列表:{my_list}")
运行结果分析:
- 写入线程的列表:[0, 1, 2, 3, 4]
- 读取线程的列表:[0, 1, 2, 3, 4]
- 主线程的列表:[0, 1, 2, 3, 4]
这说明所有线程操作的是同一个全局变量
四、进程与线程的对比分析
4.1 核心区别对比
|
特性 |
进程 |
线程 |
|
定义 |
操作系统分配资源的基本单位 |
进程内的执行单元,共享进程资源 |
|
资源开销 |
高(独立内存、资源) |
低(共享进程内存) |
|
启动速度 |
慢 |
快 |
|
并发性 |
多核并行(真正并行) |
伪并行(受 GIL 限制) |
|
数据共享 |
需 IPC 机制(队列、管道等) |
直接共享内存(需同步机制) |
|
容错性 |
高(进程崩溃不影响其他进程) |
低(线程崩溃可能导致进程终止) |
|
适用场景 |
CPU 密集型任务 |
I/O 密集型任务 |
|
Python 模块 |
multiprocessing |
threading |
4.2 关系对比
- 依附关系:线程是依附在进程里面的,没有进程就没有线程
- 创建关系:一个进程默认提供一条线程,进程可以创建多个线程
- 资源关系:进程分配资源,线程使用资源
4.3 优缺点分析
进程优缺点:
优点:
- 可以充分利用多核 CPU
- 进程间相互独立,容错性高
- 适合 CPU 密集型任务
缺点:
- 创建和切换开销大
- 进程间通信复杂
- 内存占用高
线程优缺点:
优点:
- 创建和切换开销小
- 线程间通信简单
- 内存占用低
缺点:
- Python 中受 GIL 限制,不能真正并行
- 线程间共享数据需要同步
- 一个线程崩溃可能影响整个进程
4.4 GIL(全局解释器锁)的影响
GIL 是什么:
GIL 是 Python 解释器中的一个互斥锁,它确保同一时刻只有一个线程在执行 Python 字节码
对多线程的影响:
- 在 CPU 密集型任务中,多线程无法利用多核优势
- 在 I/O 密集型任务中,由于线程会等待 I/O 操作,多线程仍然有效
解决方案:
- CPU 密集型任务:使用多进程
- I/O 密集型任务:使用多线程或协程
五、协程:轻量级并发
5.1 什么是协程
协程(Coroutine) 是用户态的轻量级线程,通过协作式多任务实现并发。相比线程,协程的切换无需操作系统调度,仅需保存寄存器上下文,因此效率更高。
核心特点:
- 轻量级:比线程更轻量,创建成本更低
- 协作式:协程主动让出 CPU 控制权
- 单线程:在一个线程内实现并发
- 高并发:单线程内可处理数千个协程
5.2 协程的优势
- 无锁机制:避免多线程同步开销
- 高并发:单线程内处理数千级 I/O 密集型任务
- 代码简洁:用同步语法写异步逻辑
- 内存高效:大量协程占用内存少
5.3 Python 中的协程实现
Python 3.5 + 引入了asyncio模块,提供了原生的协程支持。
基本语法:
import asyncio
async def task1():
"""协程任务1"""
for i in range(3):
print("任务1执行...")
await asyncio.sleep(1) # 非阻塞等待
async def task2():
"""协程任务2"""
for i in range(3):
print("任务2执行...")
await asyncio.sleep(1)
async def main():
"""主协程"""
# 并发执行多个协程
await asyncio.gather(task1(), task2())
# 运行主协程
if __name__ == "__main__":
asyncio.run(main())
关键概念:
- async def:定义协程函数
- await:暂停协程执行,等待其他协程完成
- asyncio.run():运行主协程
- asyncio.gather():并发执行多个协程
5.4 协程的核心 API
|
API |
功能描述 |
|
asyncio.create_task() |
将协程包装为任务对象 |
|
asyncio.gather() |
并发执行多个协程 |
|
asyncio.sleep() |
非阻塞式等待 |
|
asyncio.Queue |
协程安全队列 |
|
asyncio.Lock |
协程锁 |
5.5 协程的实际应用:异步爬虫
协程特别适合 I/O 密集型任务,如网络请求:
import asyncio
import aiohttp
async def fetch_url(session, url):
"""异步获取网页内容"""
async with session.get(url) as response:
content = await response.text()
return f"URL: {url}, 状态码: {response.status}, 长度: {len(content)}"
async def main():
"""主协程:并发爬取多个网页"""
urls = [
"https://www.baidu.com",
"https://www.taobao.com",
"https://www.jd.com",
"https://www.zhihu.com",
"https://www.github.com"
]
async with aiohttp.ClientSession() as session:
# 创建任务列表
tasks = [fetch_url(session, url) for url in urls]
# 并发执行所有任务
results = await asyncio.gather(*tasks)
# 打印结果
for result in results:
print(result)
if __name__ == "__main__":
asyncio.run(main())
性能优势:
- 相比同步爬虫,效率提升 10 倍以上
- 单线程内可同时处理数百个网络请求
- 避免了线程切换的开销
5.6 其他协程库
除了asyncio,Python 还有其他优秀的协程库:
(1)Gevent
from gevent import monkey
import gevent
import time
# 打补丁,使标准库支持协程
monkey.patch_all()
def task(name):
"""协程任务"""
for i in range(3):
print(f"任务{name}执行... {i}")
time.sleep(1)
def main():
"""主函数"""
# 创建协程
g1 = gevent.spawn(task, "A")
g2 = gevent.spawn(task, "B")
g3 = gevent.spawn(task, "C")
# 等待所有协程完成
gevent.joinall([g1, g2, g3])
if __name__ == "__main__":
main()
(2)Greenlet
from greenlet import greenlet
import time
def test1():
"""第一个协程"""
while True:
print("test1")
gr2.switch() # 切换到test2
time.sleep(1)
def test2():
"""第二个协程"""
while True:
print("test2")
gr1.switch() # 切换到test1
time.sleep(1)
if __name__ == "__main__":
# 创建协程对象
gr1 = greenlet(test1)
gr2 = greenlet(test2)
# 启动第一个协程
gr1.switch()
5.7 协程使用注意事项
- 避免阻塞操作:协程内禁用time.sleep()等同步 IO 操作,应使用asyncio.sleep()
- CPU 密集型任务:协程不适合 CPU 密集型任务,需结合多进程
- 异常处理:需要妥善处理协程中的异常
- 调试困难:协程的调试比线程更复杂
六、并发编程的选择策略
6.1 任务类型分析
CPU 密集型任务
- 特点:大量计算,CPU 使用率高
- 推荐方案:多进程
- 示例:数据分析、科学计算、图像处理
I/O 密集型任务
- 特点:大量等待时间(网络请求、文件读写)
- 推荐方案:协程 > 多线程
- 示例:爬虫、API 服务、文件处理
2864

被折叠的 条评论
为什么被折叠?



