Изменяет переменную класса в python threadsafe?

Я читал этот вопрос (который вам не нужно читать, потому что я буду копировать то, что есть... Я просто хотел показать вам свое вдохновение)...

Итак, если у меня есть класс, который подсчитывает, сколько экземпляров было создано:

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

Мой вопрос: если я создам объекты Foo в нескольких потоках, будет ли instance_count правильным? Являются ли переменные классов безопасными для изменения из нескольких потоков?

Ответы

Ответ 1

Это не потокобезопасно даже на 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)

Ответ 2

Нет, это не потокобезопасно. Я столкнулся с подобной проблемой несколько дней назад, и я решил реализовать блокировку благодаря декоратору. Преимущество заключается в том, что он делает код читабельным:

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



Ответ 3

Я бы сказал, что это поточно-безопасный, по крайней мере, в реализации CPython. GIL сделает все ваши "потоки" запущенными последовательно, чтобы они не могли испортить ваш счетчик ссылок.