在python中修改类变量是线程安全的吗?

在python中修改类变量是线程安全的吗?,python,multithreading,Python,Multithreading,我在读(你不必读,因为我会复制那里的东西…我只是想给你展示我的灵感) 因此,如果我有一个计算创建了多少实例的类: class Foo(object): instance_count = 0 def __init__(self): Foo.instance_count += 1 我的问题是,如果我在多个线程中创建Foo对象,实例计数是否正确?从多个线程修改类变量安全吗?我认为它是线程安全的,至少在CPython实现上是如此。GIL将使您的所有“线程”按顺序运行,这样它们就不会干扰

我在读(你不必读,因为我会复制那里的东西…我只是想给你展示我的灵感)

因此,如果我有一个计算创建了多少实例的类:

class Foo(object):
  instance_count = 0
  def __init__(self):
    Foo.instance_count += 1

我的问题是,如果我在多个线程中创建Foo对象,实例计数是否正确?从多个线程修改类变量安全吗?

我认为它是线程安全的,至少在CPython实现上是如此。GIL将使您的所有“线程”按顺序运行,这样它们就不会干扰您的引用计数。

即使在CPython上,它也不是线程安全的。请自己尝试以下内容:

import threading

class Foo(object):
    instance_count = 0

def inc_by(n):
    for i in xrange(n):
        Foo.instance_count += 1

threads = [threading.Thread(target=inc_by, args=(100000,)) for thread_nr in xrange(100)]
for thread in threads: thread.start()
for thread in threads: thread.join()

print(Foo.instance_count) # Expected 10M for threadsafe ops, I get around 5M
原因是,虽然INPLACE_ADD在GIL下是原子的,但属性仍然被加载和存储(请参见dis.dis(Foo.uu init_uu))。使用锁序列化对类变量的访问:

Foo.lock = threading.Lock()

def interlocked_inc(n):
    for i in xrange(n):
        with Foo.lock:
            Foo.instance_count += 1

threads = [threading.Thread(target=interlocked_inc, args=(100000,)) for thread_nr in xrange(100)]
for thread in threads: thread.start()
for thread in threads: thread.join()

print(Foo.instance_count)

不,它不是线程安全的。几天前我遇到了一个类似的问题,多亏了一个装饰师,我才选择实现这个锁。好处是它使代码可读:

def threadsafe_function(fn):
    """decorator making sure that the decorated function is thread safe"""
    lock = threading.Lock()
    def new(*args, **kwargs):
        lock.acquire()
        try:
            r = fn(*args, **kwargs)
        except Exception as e:
            raise e
        finally:
            lock.release()
        return r
    return new

class X:
    var = 0

    @threadsafe_function     
    def inc_var(self):
        X.var += 1    
        return X.var




根据luc的回答,下面是一个简化的decorator,它使用
上下文管理器以及一点
\uuuuuu main\uuuu
代码来加速测试。尝试使用@synchronized decorator和不使用@synchronized decorator来查看差异

import concurrent.futures
import functools
import logging
import threading


def synchronized(function):
    lock = threading.Lock()
    @functools.wraps(function)
    def wrapper(self, *args, **kwargs):
        with lock:
            return function(self, *args, **kwargs)
    return wrapper


class Foo:
    counter = 0

    @synchronized
    def increase(self):
        Foo.counter += 1


if __name__ == "__main__":
    foo = Foo()
    print(f"Start value is {foo.counter}")
    with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
        for index in range(200000):
            executor.submit(foo.increase)
    print(f"End value is {foo.counter}")


也许我不明白GIL是怎么工作的。。。但我还是没看到。无法线程1读取实例\u计数。然后thread1停止。Thread2读取实例_计数,然后停止。Thread1修改和写入。Thread2写。所以你失去了一个增量?GIL如何确保线程在整个+=操作中运行?哈,我基本上是问Sam Saffron在我之前问了什么。@Sam Saffron:我没有得到“原子工作单位”,它是对iadd方法的函数调用,所以如果你没有覆盖它,是的@汤姆:这取决于实例计数是否是不可变的。如果它是不可变的,您将丢失增量,否则不会(浮点和整数是不可变的)。woops,看起来我仍然需要学习:)Ants Aasma是非常正确的:我相信在您的第二个示例中,您希望线程目标是联锁的inc而不是inc。谢谢,更正了。有时我会被过于自由的复制和粘贴编程所困扰。谢谢你,Aasma:-)。这正是我所怀疑的。谢谢你向我证明了这一点。正如tgray所指出的,你的第二个目标应该是连锁的,但一旦你改变了这一点。。。看起来完美无缺。脱离主题,但您能否删除对异常处理程序后面的“else:”节的两个lock.release()调用?您是指在finally节中吗?在else中这样做不会在异常出现时释放使用“with”进行锁定。它大大清理了上面的问题。
Without @synchronized
End value is 198124
End value is 196827
End value is 197968

With @synchronized
End value is 200000
End value is 200000
End value is 200000