Существуют ли случаи, когда потоки Python могут безопасно манипулировать общим состоянием?

Некоторое обсуждение в другом вопросе побудило меня лучше понять случаи, когда требуется блокировка в многопоточных программах Python.

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

Однако я не смог вызвать условие гонки с помощью атомных операций, таких как добавление списка или приращение переменной. Этот тест полностью пытается продемонстрировать такую ​​гонку:

from threading import Thread, Lock
import operator

def contains_all_ints(l, n):
    l.sort()
    for i in xrange(0, n):
        if l[i] != i:
            return False
    return True

def test(ntests):
    results = []
    threads = []
    def lockless_append(i):
        results.append(i)
    for i in xrange(0, ntests):
        threads.append(Thread(target=lockless_append, args=(i,)))
        threads[i].start()
    for i in xrange(0, ntests):
        threads[i].join()
    if len(results) != ntests or not contains_all_ints(results, ntests):
        return False
    else:
        return True

for i in range(0,100):
    if test(100000):
        print "OK", i
    else:
        print "appending to a list without locks *is* unsafe"
        exit()

Я выполнил тест выше без сбоев (100x100k многопоточно добавляет). Может ли кто-нибудь заставить его потерпеть неудачу? Есть ли другой класс объекта, который может быть сделан для неправильной работы через атомарную, инкрементальную, модификацию по потокам?

Используются ли эти неявно "атомарные" семантики для других операций в Python? Связано ли это с GIL?

Ответы

Ответ 1

Добавление в список потокобезопасно, да. Вы можете добавлять только к списку, удерживая GIL, и список заботится о том, чтобы не освободить GIL во время операции append (которая, в конце концов, довольно простая операция.) Порядок, в котором идут различные операции добавления потоков через, конечно, для захватов, но все они будут строго сериализованными операциями, потому что GIL никогда не выпускается во время добавления.

То же самое не обязательно верно для других операций. Много операций на Python может привести к выполнению произвольного кода Python, что, в свою очередь, может привести к выпуску GIL. Например, i += 1 - это три различные операции: "get i", "добавить 1 к нему" и "сохранить его в i". "Добавить 1 к нему" переведет (в данном случае) в it.__iadd__(1), который может уйти и делать все, что ему нравится.

Объекты Python сами защищают собственное внутреннее состояние - dicts не будет поврежден двумя разными потоками, пытаясь установить в них элементы. Но если данные в dict должны быть внутренне согласованы, ни dict, ни GIL не делают ничего, чтобы защитить это, за исключением (в обычном режиме потока), делая его менее вероятным, но все же возможные вещи могут оказаться разными, чем вы думали.

Ответ 2

В CPython переключение потоков выполняется при выполнении байтов sys.getcheckinteval(). Таким образом, контекстный переключатель никогда не может возникать во время выполнения одного байт-кода, а операции, которые закодированы как один байт-код, по сути являются атомными и потокобезопасными, если этот байт-код не выполняет другой код Python или не вызывает код C, который выпускает GIL. Большинство операций со встроенными типами коллекций (dict, list и т.д.) Попадают в категорию "по сути нитевое".

Однако это деталь реализации, которая специфична для реализации C на Python, и на нее нельзя положиться. Другие версии Python (Jython, IronPython, PyPy и т.д.) Могут вести себя не так. Также нет гарантии, что будущие версии CPython будут поддерживать это поведение.