+ =运算符在Python中是线程安全的吗?
我想为实验创建一个非线程安全的代码块,这些是2个线程将要调用的函数。
c = 0
def increment():
c += 1
def decrement():
c -= 1
此代码线程安全吗?
如果不是,我可以理解为什么它不是线程安全的,以及哪种类型的语句通常会导致非线程安全的操作。
如果它是线程安全的,如何使它显式为非线程安全的?
8个解决方案
88 votes
不,该代码绝对是绝对线程安全的。
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解析为四个操作码:加载i,加载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
Glenn Maynard answered 2020-07-27T15:56:46Z
28 votes
(注意:每个函数都需要inc才能使代码正常工作。)
此代码线程安全吗?
否。在CPython中,只有一个字节码指令是“原子的”,即使所涉及的值是简单的整数,inc也可能不会产生单个操作码:
>>> 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实现中,结果将完全不同。 如果需要线程安全,则需要锁定机制。
bobince answered 2020-07-27T15:57:24Z
15 votes
确保我建议使用锁:
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
同步装饰器还可以帮助使代码易于阅读。
gillesv answered 2020-07-27T15:57:48Z
10 votes
很容易证明您的代码不是线程安全的。 您可以通过在关键部分使用睡眠来提高查看比赛状况的可能性(这只是模拟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
John La Rooy answered 2020-07-27T15:58:10Z
4 votes
简短的回答:不。
长答案:一般不会。
尽管CPython的GIL使单个操作码具有线程安全性,但这不是一般行为。 您可能不会假设,即使简单的操作(例如加法)也是原子指令。 当另一个线程运行时,添加可能仅完成一半。
而且,一旦函数在多个操作码中访问一个变量,线程安全性就会消失。 如果将函数体包装在锁中,则可以生成线程安全性。 但是请注意,锁可能会在计算上耗费大量资源并可能产生死锁。
ebo answered 2020-07-27T15:58:44Z
3 votes
单一操作码由于具有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
多个线程共享的每个资源都必须有一个锁。
Jochen Ritzel answered 2020-07-27T15:56:12Z
2 votes
如果您实际上想使代码不是线程安全的,并且很有可能在不尝试一万次的情况下(或在您真正不希望发生“坏的”事情发生的情况下)就发生“坏的”事情, 您可以通过显式睡眠来“抖动”您的代码:
def íncrement():
global c
x = c
from time import sleep
sleep(0.1)
c = x + 1
Rasmus Kaj answered 2020-07-27T15:59:04Z
0 votes
您确定函数递增和递减执行没有任何错误吗?
我认为它应该引发UnboundLocalError,因为您必须明确地告诉Python您要使用名为'c'的全局变量。
因此,将递增(也递减)更改为以下内容:
def increment():
global c
c += 1
我认为您的代码也是线程不安全的。 有关Python中的线程同步机制的本文可能会有所帮助。
ardsrk answered 2020-07-27T15:59:37Z