不是说Python协程不用加锁吗?

我用基于 gevent 的爬虫,如果不加锁,pymysql 的数据库操作会出错。我看很多网上教程说协程不用像线程那样加锁是优势,我还是对协程原理不是很了解, 照这样看来,写同一个文件,操作数据库,还是需要加锁的,那为什么 python 自带的 queue 在协程里面不加锁各种 get,put 也工作得很好?它是怎么做到的?
协程和线程到底有什么区别? 都是在一个进程里面切换,不加锁都会出错
不是说Python协程不用加锁吗?

8 回复

coroutine 不能完全等于单线程。因为 await 本质相当于 yield,出让当前执行栈的执行权(防止阻塞)。也就是说,同一时刻内存中存在多个,不一定相互重叠的执行栈。说白了,多线程的调度是操作系统的 scheduler,而 coroutine 的调度是你的代码。


协程不用加锁这个说法得看具体场景。在单线程的asyncio里,如果一个协程在执行过程中不会被其他协程打断(比如没有await),那操作共享数据确实是安全的,因为同一时间只有一个协程在跑。但一旦你await了,比如等个网络请求或者asyncio.sleep,事件循环就可能去执行别的协程了。如果多个协程交替读写同一个变量,还是会有竞态条件。

看个例子:

import asyncio

counter = 0

async def increment():
    global counter
    temp = counter
    await asyncio.sleep(0)  # 这里主动让出控制权
    counter = temp + 1

async def main():
    global counter
    tasks = [asyncio.create_task(increment()) for _ in range(1000)]
    await asyncio.gather(*tasks)
    print(f"Final counter value: {counter}")  # 结果大概率不是1000

asyncio.run(main())

上面这个increment函数不是原子操作,await前后可能被其他协程插队,最后counter的值会小于1000。要解决这个问题,可以用asyncio.Lock

import asyncio

counter = 0
lock = asyncio.Lock()

async def safe_increment():
    global counter
    async with lock:
        temp = counter
        await asyncio.sleep(0)
        counter = temp + 1

async def main():
    global counter
    tasks = [asyncio.create_task(safe_increment()) for _ in range(1000)]
    await asyncio.gather(*tasks)
    print(f"Final counter value: {counter}")  # 这次稳稳的是1000

asyncio.run(main())

所以结论是:协程在单线程内虽然避免了操作系统线程的切换开销,但只要存在await让出控制权的点,对共享资源的非原子操作依然需要锁(asyncio.Lock)来保证安全。简单说就是,有await的竞争访问就得加锁。

至于 queue 为何"正确"(undefined behavior),不如贴出你的测试代码,看看是否真的实现了 await 操作。

具体事情具体讨论,并没有说协程就一定不加锁,协程本身的某些功能实现也是通过线程池实现的;就算没有用到其他线程,假如一个操作需要连续性地完成,那么也是需要借助锁的概念。asyncio 和 gevent 库中都有实现 threading 下的一些同步机制,比如 Lock、Semaphore 等,虽然和 threading 的底层的实现不同,但是同样起到了保证一些操作顺序、不会被打断地执行。

另外,queue 自带锁

> 协程是用户态相互协作的线程

你看,本质上还是线程,所以还是可能存在竞争条件,所以需要加锁

谁和你说不用加锁的…

协程都是平等、并行的多个任务,它本身并不是多线程,每次运行的都是某个代码片段,go 是通过多线程来执行这些代码片段,才让大家误解。

但协程的代码里面有可能会调用到底层的 C 代码,如很多库就是 C 写的,如果这个库没做好多线程的锁操作,就会出现帖子说的事。这也说明这个库实现有严重问题


有兴趣可以去看看 OpenResty、Nginx+Lua,也是协程的世界。但这个世界里连“锁”的任何操作方法都没暴露给开发者,就是因为协程本身是不需要锁的。底层的事情就应该完整隐藏起来,这样使用者才不会遇到这种摸不着头脑的事

补充一下, Python 的一些数据结构是线程安全的(比如 collections 模块的 deque 结构), 使用这些结构多线程就可以不用加锁.

回到顶部