# 聊聊Python里的Concurrency
今天想和大家聊聊Python里一个经常被讨论的话题——concurrency。这个词翻译过来叫“并发”,听起来有点学术,但理解它对我们写出高效的程序特别有帮助。
他是什么
Concurrency不是并行。很多人容易把这两个概念搞混,其实它们有本质区别。并行是真正的同时执行多个任务,就像你有两个厨师同时在两个灶台上炒菜。而并发更像是同一个厨师在多个灶台间快速切换,看起来像是在同时做几道菜,但实际上每个时刻只处理一个任务。
在Python的世界里,concurrency主要解决的是I/O密集型任务等待时的效率问题。比如你的程序需要从网络下载文件、访问数据库、或者读写磁盘,这些操作大部分时间都在等待外部系统的响应。这时候CPU其实是空闲的,concurrency就是让CPU在等待一个任务的时候去处理其他任务。
他能做什么
想象一下你正在写一个网络爬虫。如果不用concurrency,程序会先请求第一个网页,等完全收到响应后再请求第二个,这样效率很低。用了concurrency之后,程序可以同时发起多个请求,哪个先返回就先处理哪个。
再比如你写了一个Web服务器,每个用户请求都需要查询数据库。如果没有concurrency,服务器只能一个个处理请求,后面的用户就得排队等着。用了concurrency,服务器可以同时处理多个请求,用户体验会好很多。
但要注意,concurrency对计算密集型的任务帮助不大。如果你的程序主要是做数学计算、图像处理这类CPU密集型工作,concurrency可能反而会增加开销。
怎么使用
Python提供了几种实现concurrency的方式,每种都有适合的场景。
最经典的是多线程,通过threading模块实现。线程比较轻量,创建和切换的开销小,适合I/O密集的场景。但Python有个全局解释器锁(GIL),这导致多个线程无法真正并行执行CPU密集型任务。不过对于I/O密集型任务,GIL的影响没那么大,因为线程在等待I/O时会释放GIL。
协程是另一种方式,通过asyncio库实现。协程比线程更轻量,切换开销更小。它使用async/await语法,代码写起来像是同步的,但实际上是异步执行。协程特别适合高并发的网络应用。
还有多进程,通过multiprocessing模块实现。每个进程有自己的Python解释器和内存空间,可以绕过GIL的限制,真正实现并行。但进程间通信比线程间通信开销大,适合CPU密集型且任务间不需要频繁通信的场景。
选择哪种方式,要看具体需求。一般来说,I/O密集型用线程或协程,CPU密集型用多进程,需要高并发网络服务优先考虑协程。
最佳实践
写concurrent代码有些坑需要注意。共享数据是个大问题,多个线程或协程同时修改同一个变量很容易出问题。这时候需要用锁来保护,但锁用不好又可能导致死锁。
任务之间的协调也很重要。比如要等所有下载任务都完成后再进行下一步处理,就需要用一些同步原语。asyncio里的gather、wait_for这些工具用起来挺方便的。
错误处理在concurrent环境下更复杂。一个任务出错不应该影响其他任务,但又要能捕获和处理错误。asyncio的Task可以单独取消,错误也可以单独处理,设计得比较周到。
资源限制也值得考虑。比如同时发起太多网络请求可能会把对方服务器搞垮,或者被当成攻击。这时候可以用信号量或者连接池来控制并发数。
调试concurrent程序比较麻烦,因为问题可能不是每次都能复现。加日志是个好办法,但要注意日志本身也可能成为性能瓶颈。
和同类技术对比
和其他语言相比,Python的concurrency有其特点。像Go语言,它的goroutine非常轻量,可以轻松创建成千上万个,而且调度器做得很好。Java的线程相对重量级,但JVM的优化很成熟。Node.js天生就是异步的,但回调地狱曾经是个问题。
Python的asyncio借鉴了很多其他语言的经验,用起来越来越顺手。不过Python的生态太庞大了,很多库还没有适配asyncio,这是实际开发中常遇到的问题。
在Python自己的生态里,concurrent.futures模块提供了线程池和进程池的高级接口,用起来比直接操作threading或multiprocessing简单。Celery这类分布式任务队列适合更大的系统,可以把任务分发到多台机器上执行。
说到底,没有哪种技术是银弹。选择concurrency方案时要考虑团队熟悉程度、项目需求、第三方库支持等多种因素。有时候简单的方案反而更可靠,过度设计可能带来不必要的复杂度。
Concurrency是个很有用的工具,但也不是所有问题都需要用它解决。先想清楚要解决什么问题,再选择合适的工具,这才是专业的做法。代码不仅要能跑,还要好维护、好扩展,这些比单纯追求性能更重要。