跳转到内容

并发编程基础

并发编程是现代软件开发中的核心技能。当程序需要同时处理多个任务——比如响应用户请求、抓取网页数据、实时处理传感器数据——并发编程就显得尤为重要。

这一章介绍并发编程的基本概念,解释并发与并行的区别,以及Python中实现并发的几种主要方式。

并发与并行的区别

很多人容易混淆并发和并行,认为它们是一回事。实际上,它们有本质的区别。

并行是指多个任务同时执行,需要多个CPU核心。在多核处理器上,不同核心可以真正同时执行不同的任务。对于CPU密集型任务,比如矩阵运算、图像处理,并行能显著提升性能。

并发是指多个任务交替执行,在同一个时间窗口内完成更多工作。并发可以在单核CPU上实现,通过快速切换执行不同任务,营造出"同时运行"的错觉。并发适合IO密集型任务,因为程序在等待IO时可以切换去执行其他任务。

一个形象的比喻:厨师做早餐。并行是同时使用多个炉子同时煎蛋、烤面包、煮咖啡。并发是这个厨师在烤面包的等待时间里,开始煎蛋,然后在煎蛋的等待时间里去倒咖啡。通过交替执行,在同样的时间内完成更多工作。

现代计算机通常有多核CPU,所以并行和并发可以结合使用:多个核心各自运行并发执行的任务。

Python的GIL限制

理解Python的全局解释器锁(GIL)是理解Python并发的前提。

GIL是CPython解释器中的一个机制,同一时间只有一个线程执行Python字节码。这意味着,即使用threading模块创建多个线程,在任意时刻只有一个线程在真正执行。

GIL的存在是因为CPython的内存管理不是线程安全的。Python使用引用计数来管理对象,多个线程同时修改引用计数可能导致内存泄漏或野指针。GIL确保了CPython的线程安全,但也限制了真正的并行执行。

对于CPU密集型任务,多线程由于GIL的存在反而比单线程更慢——线程切换有开销,而GIL让线程无法真正并行。对于IO密集型任务,多线程仍然有效,因为线程在等待IO时可以释放GIL,让其他线程继续执行。

突破GIL限制的方式有几种:使用multiprocessing创建多进程,每个进程有独立的GIL;使用C扩展释放GIL;或者使用异步编程。

多进程与多线程的选择

不同的并发方式适合不同的场景。

多线程适合IO密集型任务:网络请求、文件读写、数据库操作。在这类任务中,线程大部分时间在等待,释放GIL让其他线程执行。多线程的创建和切换开销较小,适合大量并发连接。

多进程适合CPU密集型任务:数值计算、数据处理、图像处理。每个进程有独立的GIL,可以真正并行执行。进程比线程更重量级,但提供了完全的隔离。

协程适合高并发IO场景:大规模网络爬虫、实时通信服务器。协程是用户态的线程切换,开销极低,但只能实现单线程并发。协程适合处理大量IO等待,但无法利用多核。

选择建议:如果是网络爬虫或API服务,异步编程最合适;如果是科学计算或数据分析,多进程更合适;如果是GUI程序中的后台任务,多线程更合适。

同步与异步模型

同步模型中,任务按顺序执行,一个任务完成后才开始下一个。同步代码容易理解和调试,但效率较低。

异步模型中,多个任务可以交替执行。程序不需要阻塞等待IO完成,可以继续执行其他工作。异步代码复杂度较高,但吞吐量更大。

python
同步方式:
request1()  # 等待完成
request2()  # 再等待完成
# 总时间 = request1时间 + request2时间

异步方式:
await request1()  # 开始request1
await request2()  # 开始request2,同时request1在后台运行
# 总时间 = max(request1时间, request2时间)

Python中实现异步的主要方式是asyncio模块。asyncio基于事件循环,在单线程内实现并发。它使用async/await语法,让异步代码看起来像同步代码。

进程、线程、协程对比

特性多进程多线程协程
GIL限制
内存占用
切换开销极小
通信方式Queue/Pipe共享内存await
适用场景CPU密集型IO密集型高并发IO
稳定性进程隔离共址运行单线程

进程之间相互隔离,一个进程崩溃不会影响其他进程。线程共享同一进程的内存空间,通信方便但需要处理同步问题。协程完全运行在单线程内,没有线程安全问题,但一个协程崩溃可能影响整个事件循环。

常见的并发问题

并发编程虽然强大,但也带来了新的挑战。

竞态条件是最常见的问题。当多个线程或进程同时访问共享资源,且结果取决于访问的顺序时,就存在竞态条件。比如两个线程同时给计数器加1,如果操作不是原子的,最终结果可能只加了1而不是2。

死锁发生在多个任务相互等待对方释放资源时。比如线程A持有锁1等待锁2,线程B持有锁2等待锁1,两个线程都无法继续执行。

活锁是死锁的变种,任务虽然没有阻塞,但不断重复相同的操作。比如两个礼貌的人同时站在门口让路给对方,结果两人都无法通过。

饥饿指某些任务始终无法获得所需资源。比如高优先级任务不断抢占资源,低优先级任务长期无法执行。

理解和预防这些问题,是编写健壮并发程序的基础。

Python并发模块概览

Python标准库提供了丰富的并发编程支持。

multiprocessing模块提供进程管理,支持进程创建、进程间通信、同步原语。适用于CPU密集型任务的并行化。

threading模块提供线程管理,创建和管理线程的API与multiprocessing类似。但由于GIL限制,对CPU密集型任务帮助不大。

asyncio模块提供协程支持,基于事件循环实现单线程并发。是Python 3.5引入的现代异步编程方式。

concurrent.futures模块提供高级接口,封装了进程池和线程池,简化了并行任务的提交和结果获取。

选择合适的模块,取决于任务的特性和性能要求。

基于 MIT 许可发布