是+;=Python中的运算符线程安全?
我想为实验创建一个非线程安全的代码块,这是两个线程将要调用的函数是+;=Python中的运算符线程安全?,python,thread-safety,increment,Python,Thread Safety,Increment,我想为实验创建一个非线程安全的代码块,这是两个线程将要调用的函数 c = 0 def increment(): c += 1 def decrement(): c -= 1 这段代码是线程安全的吗 如果不是,我可以理解为什么它不是线程安全的,以及什么样的语句通常会导致非线程安全的操作 如果它是线程安全的,如何使它显式地非线程安全?简短回答:否 长句回答:一般不会 虽然CPython的GIL制造单操作码,但这并不是一般行为。您可能不会认为,即使是简单的操作(如加法)也是原子指令。当另
c = 0
def increment():
c += 1
def decrement():
c -= 1
这段代码是线程安全的吗
如果不是,我可以理解为什么它不是线程安全的,以及什么样的语句通常会导致非线程安全的操作
如果它是线程安全的,如何使它显式地非线程安全?简短回答:否
长句回答:一般不会
虽然CPython的GIL制造单操作码,但这并不是一般行为。您可能不会认为,即使是简单的操作(如加法)也是原子指令。当另一个线程运行时,加法只能完成一半
一旦函数访问多个操作码中的变量,线程安全性就消失了。如果将函数体封装在中,则可以生成线程安全性。但是请注意,锁的计算成本可能很高,并且可能会产生死锁。(注意:每个函数中都需要global c
,才能使代码正常工作。)
这段代码是线程安全的吗
否。在CPython中,只有一条字节码指令是“原子”指令,+=
可能不会产生一个操作码,即使涉及的值是简单整数:
>>> c= 0
>>> def inc():
... global c
... c+= 1
>>> import dis
>>> dis.dis(inc)
3 0 LOAD_GLOBAL 0 (c)
3 LOAD_CONST 1 (1)
6 INPLACE_ADD
7 STORE_GLOBAL 0 (c)
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
因此,一个线程可以在加载c和1的情况下到达索引6,放弃GIL,让另一个线程进入,该线程执行inc
并休眠,将GIL返回到第一个线程,该线程现在的值错误
在任何情况下,原子的是一个实现细节,您不应该依赖它。字节码在未来的CPython版本中可能会发生变化,而在其他不依赖GIL的Python实现中,结果将完全不同。如果需要线程安全性,则需要一个锁定机制。由于GIL,单操作码是线程安全的,除此之外:
import time
class something(object):
def __init__(self,c):
self.c=c
def inc(self):
new = self.c+1
# if the thread is interrupted by another inc() call its result is wrong
time.sleep(0.001) # sleep makes the os continue another thread
self.c = new
x = something(0)
import threading
for _ in range(10000):
threading.Thread(target=x.inc).start()
print x.c # ~900 here, instead of 10000
由多个线程共享的每个资源必须有一个锁。您确定函数递增和递减执行时没有任何错误吗 我认为它应该引发UnboundLocalError,因为您必须显式地告诉Python您想要使用名为“c”的全局变量 因此,将增量(也是减量)更改为以下值:
def increment():
global c
c += 1
我认为你的代码是线程不安全的。关于Python中的线程同步机制可能会有所帮助。如果您真的想让代码不是线程安全的,并且在不尝试一万次的情况下(或者当您真的不想让“坏”事情发生的时候),就很有可能真的发生“坏”事情,那么您可以通过显式休眠来“抖动”代码:
def íncrement():
global c
x = c
from time import sleep
sleep(0.1)
c = x + 1
很容易证明代码不是线程安全的。您可以通过在关键部分使用睡眠来增加看到竞争状况的可能性(这只是模拟一个缓慢的CPU)。但是,如果您运行代码足够长的时间,您最终应该会看到竞争条件
from time import sleep
c = 0
def increment():
global c
c_ = c
sleep(0.1)
c = c_ + 1
def decrement():
global c
c_ = c
sleep(0.1)
c = c_ - 1
不,这段代码绝对不是线程安全的
import threading
i = 0
def test():
global i
for x in range(100000):
i += 1
threads = [threading.Thread(target=test) for t in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
assert i == 1000000, i
总是失败
i+=1解析为四个操作码:load i,load 1,将两个操作码相加,并将其存储回i。Python解释器每100个操作码切换一个活动线程(通过从一个线程释放GIL以便另一个线程可以拥有它)。(这两个都是实现细节。)当100操作码抢占发生在加载和存储之间,允许另一个线程开始递增计数器时,竞争条件就会发生。当它返回到挂起的线程时,它继续使用旧的值“i”,同时撤消其他线程运行的增量
使其线程安全是简单的;添加锁:
#!/usr/bin/python
import threading
i = 0
i_lock = threading.Lock()
def test():
global i
i_lock.acquire()
try:
for x in range(100000):
i += 1
finally:
i_lock.release()
threads = [threading.Thread(target=test) for t in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
assert i == 1000000, i
为了确保安全,我建议使用锁:
import threading
class ThreadSafeCounter():
def __init__(self):
self.lock = threading.Lock()
self.counter=0
def increment(self):
with self.lock:
self.counter+=1
def decrement(self):
with self.lock:
self.counter-=1
同步装饰器还可以帮助保持代码易于阅读。任何东西都有多个操作码/是一个复合的,除非另有说明。简短和冗长的答案都是否定的。比公认的答案更有用。谢谢投票赞成。如果锁是针对每个增量而不是每100000个增量获取和释放的,那么您的锁示例将更具说明性。如果线程要按顺序执行而没有任何重叠,为什么还要麻烦线程呢?对这类东西使用睡眠是非常错误的。你是怎么得出0.1的值的?更快的处理器需要更长的睡眠时间吗?用睡眠来解决问题几乎总是错误的。@omribahumi,什么?我想你被我回答的目的弄糊涂了。这段代码是一个很容易证明一段特定代码不是线程安全的例子。睡眠只是一个占位符,用来模拟正常情况下的额外处理。如果你的意思是使用睡眠是避免比赛条件的错误方式,我当然同意,但这不是我的答案所宣称的。@jacmkno,答案没有错,但出于某种原因,它让人们感到困惑。它证明了OP的代码不是线程安全的。或者你是在暗示其他人吗?这纯粹是因为你似乎因为其他人没有阅读你的答案而受到惩罚。。。对我来说很有意义在每个函数开始时都应该有一个
全局c
减速,否则这真的没有什么作用。嗨,努贝拉,你能选择正确的答案吗,这样未来的读者就不会感到困惑了?这并没有回答关于+=
的问题,如果我错了,请纠正我,print x.c
不会等待线程完成。因此,当您打印输出时,大多数线程仍在运行。是否要更新答案,其中提到只有在处理共享/全局变量时,线程安全才是一个问题。在您的示例中,x是一个全局变量。