Multithreading code run by CPython with GIL and IronPython without GIL

2022年10月27日

CPython has a global interpreter lock such that two threads cannot run simultaneously on a multi-core CPU. The context switch (thread switch) duration, by default, is 0.05 second, which means a thread runs up to 0.05 second and then the execution switches to another thread.

from threading import Thread

a = 0

def f():
	global a
	# print('start')
	for _ in range(100000):
		a = a + 1
		a = a + 1
		# print(a)
		# time.sleep(0.1)


if __name__ == '__main__':
	t1 = Thread(target=f, daemon=True)
	t1.start()

	t2 = Thread(target=f, daemon=True)
	t2.start()

	t1.join()
	t2.join()

	print(f'Final {a}')

In this code, I start two threads executing the same function f. The two threads manipulate the same variable a and increment by 2. a = a + 1 is not an atomic operation by Python specification[1] and generates two bytecodes by CPython (you can check it with the dis package). Although context switch may happen between the two bytecodes, in reality this rarely does. The figure shows about one of ten runs miscalculate the value.

If we run the same code in IronPython, which has no GIL and allows threads to run in parallel, the code almost never sums the correct value.

This is the result of running python-nogil on a Linux box.

The code here thus serves a purpose that when you are given a Python implementation that you don’t know if it has a global lock, you can run the code. If the final value is often calculated to 400000, this Python implementation runs the code in a more serial manner, which implies a global lock.

参考资料

  1. . What kinds of global value mutation are thread-safe?. Python. [2022-10-28].