我已经读过an article有关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))
当我使用Python 2.7.15时,它确实返回与文章相同的结果。但是当我使用Python 3.6.9时却没有(所有线程返回相同的结果= 200000)。
我不知道新的GIL实现(自Python 3.2起)是否处理了竞争条件问题?如果可以,为什么在Python> 3.2中仍然存在Lock,Mutex。如果不是这样,为什么在运行多线程以修改共享资源(如上例)时没有冲突?
这些天,当我试图更多地了解Python真正如何工作时,我的想法一直困扰于这些问题。
您所指的更改是用开关间隔代替检查间隔。这意味着,与其每100字节代码切换一次线程,不如每5毫秒这样做一次。
参考:https://pymotw.com/3/sys/threads.html https://mail.python.org/pipermail/python-dev/2009-October/093321.html
因此,如果您的代码运行得足够快,它将永远不会经历线程切换,并且在您看来操作实际上是原子操作时,您可能会发现它们是原子操作。由于没有实际的线程交织,因此没有出现竞争条件。 x += 1
实际上是四个字节的代码:
>>> dis.dis(sync.increment)
11 0 LOAD_GLOBAL 0 (x)
3 LOAD_CONST 1 (1)
6 INPLACE_ADD
7 STORE_GLOBAL 0 (x)
10 LOAD_CONST 2 (None)
13 RETURN_VALUE
解释器中的线程切换可以在任何两个字节码之间发生。
考虑到在2.7中这总是打印200000,因为检查间隔设置得太高,以至于每个线程在下一个运行之前就完整地完成了。可以用切换间隔来构造相同的对象。
import sys
import threading
print(sys.getcheckinterval())
sys.setcheckinterval(1000000)
# 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))
GIL保护个人字节码指令。相反,竞争条件是incorrect ordering of instructions,表示multiple字节码指令。结果,GIL cannot可以防止Python VM自身以外的竞争情况。
但是,就其本质而言,种族条件并不总是会触发。某些GIL策略或多或少会触发某些竞赛条件。短于GIL窗口的线程永远不会中断,而长于GIL窗口的线程总是会中断。
您的increment
函数有6个字节的代码指令,内部循环也有调用。其中,必须立即完成4条指令,这意味着有3个可能的切换点会破坏结果。您的整个thread_task
功能大约需要0.015s至0.020s(在我的系统上)。
使用旧的GIL每100条指令进行切换,则保证每8.3次调用或大约1200次中断。新的GIL每5ms切换一次,循环仅中断3次。