为什么协程的全局变量无需加锁
请问下面的输出是?
import eventlet
import threading
count = 0
def count_10000():
global count
for i in xrange(10000):
count += 1
def count_in_threads():
threads = []
for i in xrange(5):
t = threading.Thread(target=count_10000)
threads.append(t)
t.start()
# wait all threads to finish
for t in threads:
t.join()
def count_in_coroutines():
pool = eventlet.GreenPool()
for i in xrange(5):
pool.spawn_n(count_10000)
# wait all coroutines to finish
pool.waitall()
count_in_threads()
print count
count = 0
count_in_coroutines()
print count
本机的运行结果如下:
19598
50000
事实上,不论是在单核的 CPU 还是在多核的 CPU,多线程下 count 的值是不确定的(介于 1 至 50000),多协程下 count 值必定为 50000,stackoverflow 也有类似的问题 why-use-threading-data-race-will-occur-but-will-not-use-gevent。
一个 Python 进程内,任何时刻只有一个协程在运行,所以协程本质上是伪并发的。有人会问,由于 Python 全局解释锁(Global Interpreter Lock)的存在,一个 Python 进程内任何时刻同样仅有一个线程在运行,为什么多线程下就会出现 race condition 呢?
原图出处 UnderstandingGIL
和线程不同,协程由应用程序负责调度,操作系统并不感知。操作系统在切换线程的时机是不确定的,但是应用程序切换协程是有条件的。应用程序只有在以下场景才会切换协程:
- sleep:如 eventlet.sleep()
- IO:比如网络 IO,磁盘 IO 等。
所以协程在执行 count += 1 时不会被切换,保证了该操作的原子性,从 CPU 的角度来看,count += 1 可以分为三个步骤:
- 读取数据 count
- count 加 1
- 写回数据 count
由于线程的切换是随机的,不能保证 count += 1 的原子性,所以就有可能出现如下的 race condiction:
因此多线程下 count 的值不确定,但是介于 1 到 50000 之间。
综上,Python 多协程下的全局变量之所以不需要加锁,是因为以下两个条件保证了它不会出现 race condiction:
- 一个进程内,任何时候只有一个协程在运行。
- 协程的切换是有条件的,它只有在遇上 IO 和 sleep 等场景时才会触发切换。