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

时间:2009-07-02 06:38:59

标签: python multithreading

我正在阅读this question(你不必阅读,因为我会复制那里的东西......我只是想告诉你我的灵感)......

所以,如果我有一个类来计算创建了多少个实例:

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

我的问题是,如果我在多个线程中创建Foo对象,instance_count是否正确?是否可以安全地从多个线程修改类变量?

4 个答案:

答案 0 :(得分:24)

即使在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 .__ init __))。使用锁定序列化对类变量的访问:

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)

答案 1 :(得分:8)

不,它不是线程安全的。我几天前遇到过类似的问题,我选择通过装饰器实现锁定。好处是它使代码可读:

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

答案 2 :(得分:0)

在luc的回答之后,这是一个简化的装饰器,它使用filter上下文管理器和少量with代码来加速测试。在不使用@synchronized装饰器的情况下进行尝试,以查看区别。

__main__
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}")

答案 3 :(得分:-4)

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