在Python中修改类变量线程安全吗?
我在看这个问题(你不需要去看,因为我会把里面的内容复制过来... 我只是想让你知道我的灵感来源)...
所以,如果我有一个类可以计算创建了多少个实例:
class Foo(object):
instance_count = 0
def __init__(self):
Foo.instance_count += 1
我的问题是,如果我在多个线程中创建 Foo 对象,instance_count 的值会正确吗?类变量在多个线程中修改是安全的吗?
5 个回答
1
接着luc的回答,这里有一个简化版的装饰器,使用了with
上下文管理器,还有一些__main__
代码来启动测试。你可以试着在有和没有@synchronized装饰器的情况下运行,看看有什么不同。
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}")
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
14
不,这个不是线程安全的。我几天前遇到了类似的问题,我选择使用装饰器来实现锁。这样做的好处是让代码更易读:
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
34
即使在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
原因是,虽然在GIL(全局解释器锁)下,INPLACE_ADD是原子的,但属性的加载和存储还是会发生(可以查看一下 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)