Есть ли случаи, когда потоки Python могут безопасно управлять общим состоянием?
Некоторое обсуждение в другом вопросе побудило меня лучше понять случаи, когда в многопоточных программах на Python требуется блокировка.
В этой статье о потоках в 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()
Я запустил тест выше без сбоев (100x 100k многопоточных добавлений). Кто-нибудь может заставить его потерпеть неудачу? Есть ли другой класс объекта, который можно заставить себя вести себя неправильно с помощью атомарной, инкрементной модификации потоками?
Применяется ли эта неявная "атомарная" семантика к другим операциям в Python? Это напрямую связано с GIL?
2 ответа
Добавление к списку поточно-ориентировано, да. Вы можете добавить к списку только удерживая GIL, и этот список не выпускает GIL во время append
операция (которая, в конце концов, довольно простая операция). Порядок, в котором выполняются операции добавления различных потоков, конечно же, захватывает, но все они будут строго сериализованными операциями, потому что GIL никогда не освобождается во время добавления.
То же самое не обязательно верно для других операций. Много операций в Python может привести к выполнению произвольного кода Python, что, в свою очередь, может привести к освобождению GIL. Например, i += 1
это три разные операции "получить i
', "добавить 1 к нему" и "сохранить его в i
". добавить 1 к нему" будет переводить (в этом случае) в it.__iadd__(1)
, который может уйти и делать все, что угодно.
Сами объекты Python защищают свое собственное внутреннее состояние - дикты не будут повреждены двумя разными потоками, пытающимися установить в них элементы. Но если данные в dict должны быть внутренне непротиворечивыми, ни dict, ни GIL ничего не предпринимают, чтобы защитить это, за исключением (обычным способом потока), делая его менее вероятным, но все же возможным, вещи в конечном итоге будут другими, чем вы думали.
В CPython переключение потоков выполняется после выполнения кодов sys.getcheckinteval(). Таким образом, переключение контекста никогда не может происходить во время выполнения одного байт-кода, и операции, которые закодированы как один байт-код, по своей природе являются атомарными и потокобезопасными, если только этот байт-код не выполняет другой код Python или не вызывает код C, который выпускает GIL. Большинство операций со встроенными типами коллекций (dict, list и т. Д.) Попадают в категорию "по сути потокобезопасных".
Однако это деталь реализации, специфичная для C-реализации Python, и на нее не следует полагаться. Другие версии Python (Jython, IronPython, PyPy и т. Д.) Могут вести себя не так. Также нет гарантии, что будущие версии CPython сохранят это поведение.