是+;=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是一个全局变量。