Python有GIL为何仍需线程同步?竞态条件成因解惑
GIL下Python多线程竞态条件的成因解释
GIL的定义
根据维基百科对GIL的定义:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython's memory management is not thread-safe.
问题背景
很多人会误以为,因为CPython的GIL同一时间只允许一个线程执行Python字节码,所以多线程操作共享变量时不会出现竞态条件,不需要做线程同步。但实际运行如下代码时,却出现了竞态条件,得到不符合预期的结果:
示例代码
import threading # global variable x x = 0 def increment(): """ function to increment global variable x """ global x x += 1 def thread_task(): """ task for thread calls increment function 100000 times. """ for _ in range(100000): increment() def main_task(): global x # setting global variable x as 0 x = 0 # creating threads t1 = threading.Thread(target=thread_task) t2 = threading.Thread(target=thread_task) # start threads t1.start() t2.start() # wait until threads finish their job t1.join() t2.join() if __name__ == "__main__": for i in range(10): main_task() print("Iteration {0}: x = {1}".format(i,x))
运行输出
Iteration 0: x = 175005 Iteration 1: x = 200000 Iteration 2: x = 200000 Iteration 3: x = 169432 Iteration 4: x = 153316 Iteration 5: x = 200000 Iteration 6: x = 167322 Iteration 7: x = 200000 Iteration 8: x = 169917 Iteration 9: x = 153589
成因解释
核心原因在于:GIL保证的是同一时间只有一个线程执行Python字节码,但单个Python语句(比如x += 1)并不是原子操作。
x += 1在底层会被拆解成三个独立的字节码指令:
- 加载全局变量
x的值到栈顶(LOAD_GLOBAL x) - 将栈顶的值加1(
INPLACE_ADD) - 将计算结果存回全局变量
x(STORE_GLOBAL x)
GIL会在字节码执行的间隙释放(比如执行完一定数量的字节码后,或者遇到I/O操作时)。假设线程A执行完前两个字节码(已经把x的值加载并加1),此时GIL被释放,线程B获得GIL并执行完整的三个字节码,把x的值更新。之后线程A重新获得GIL,执行第三个字节码,把自己之前计算的结果存回x,这就覆盖了线程B的更新,导致最终x的数值比预期的200000小。
简单说,GIL只保护字节码级别的原子性,但用户层面的单个语句可能对应多个字节码,这些字节码之间可能被其他线程打断,从而引发竞态条件。因此,多线程操作共享变量时,仍然需要使用线程同步机制(比如threading.Lock)来保证操作的原子性。
内容的提问来源于stack exchange,提问作者Swastik
相关产品推荐
相关产品推荐

