请问下面的输出是?

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 呢?

python gil lock                                                           原图出处 UnderstandingGIL

和线程不同,协程由应用程序负责调度,操作系统并不感知。操作系统在切换线程的时机是不确定的,但是应用程序切换协程是有条件的。应用程序只有在以下场景才会切换协程:

  • sleep:如 eventlet.sleep()
  • IO:比如网络 IO,磁盘 IO 等。

所以协程在执行 count += 1 时不会被切换,保证了该操作的原子性,从 CPU 的角度来看,count += 1 可以分为三个步骤:

  • 读取数据 count
  • count 加 1
  • 写回数据 count

由于线程的切换是随机的,不能保证 count += 1 的原子性,所以就有可能出现如下的 race condiction:

race condition

因此多线程下 count 的值不确定,但是介于 1 到 50000 之间。

综上,Python 多协程下的全局变量之所以不需要加锁,是因为以下两个条件保证了它不会出现 race condiction:

  • 一个进程内,任何时候只有一个协程在运行。
  • 协程的切换是有条件的,它只有在遇上 IO 和 sleep 等场景时才会触发切换。