57

I want to create a non-thread-safe chunk of code for experimentation, and those are the functions that 2 threads are going to call.

c = 0

def increment():
  c += 1

def decrement():
  c -= 1

Is this code thread safe?

If not, may I understand why it is not thread safe, and what kind of statements usually lead to non-thread-safe operations.

If it is thread-safe, how can I make it explicitly non-thread-safe?

4

8 回答 8

117

不,这段代码是绝对的,显然不是线程安全的。

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
于 2009-11-11T23:37:10.687 回答
31

(注意:您需要global c在每个函数中才能使您的代码正常工作。)

这个代码线程安全吗?

不。在 CPython 中只有一个字节码指令是“原子的”,并且 a+=可能不会产生单个操作码,即使所涉及的值是简单的整数:

>>> 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 并让另一个线程进入,该线程执行 aninc并休眠,将 GIL 返回到第一个线程,该线程现在具有错误的值。

在任何情况下,什么是原子的都是你不应该依赖的实现细节。字节码在 CPython 的未来版本中可能会发生变化,结果在其他不依赖 GIL 的 Python 实现中将完全不同。如果你需要线程安全,你需要一个锁定机制。

于 2009-11-11T19:33:33.920 回答
16

可以肯定的是,我建议使用锁:

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

同步装饰器还可以帮助保持代码易于阅读。

于 2011-07-20T11:27:40.543 回答
12

很容易证明你的代码不是线程安全的。您可以通过在关键部分使用睡眠来增加看到竞争条件的可能性(这只是模拟慢速 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
于 2009-11-11T19:39:29.183 回答
5

简短的回答:没有。

长答案:一般不会。

虽然 CPython 的 GIL 使单个操作码成为线程安全的,但这不是一般行为。您可能不会认为即使是像加法这样的简单操作也是原子指令。当另一个线程运行时,添加可能只完成了一半。

一旦你的函数在多个操作码中访问一个变量,你的线程安全就消失了。如果将函数体包装在locks中,则可以生成线程安全。但请注意,锁的计算成本可能很高,并且可能会产生死锁。

于 2009-11-11T19:29:34.257 回答
2

由于 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

多个线程共享的每个资源都必须有一个锁。

于 2009-11-11T19:34:44.713 回答
2

如果你真的想让你的代码不是线程安全的,并且很有可能在你不尝试一万次的情况下实际发生“坏”的事情(或者当你真的希望“坏”的事情发生的时候),您可以通过显式睡眠来“抖动”您的代码:

def íncrement():
    global c
    x = c
    from time import sleep
    sleep(0.1)
    c = x + 1
于 2009-11-11T19:37:02.313 回答
0

您确定函数递增和递减执行没有任何错误吗?

我认为它应该引发 UnboundLocalError,因为您必须明确告诉 Python 您要使用名为“c”的全局变量。

因此将增量(也减量)更改为以下内容:

def increment():
    global c
    c += 1

我认为你的代码是线程不安全的。这篇关于 Python 中线程同步机制的文章可能会有所帮助。

于 2009-11-11T19:36:53.687 回答