Подтвердить что ты не робот

Расширяет ли список Python (например, l + = [1]), гарантированный потокобезопасностью?

Если у меня есть целое число i, небезопасно делать i += 1 для нескольких потоков:

>>> i = 0
>>> def increment_i():
...     global i
...     for j in range(1000): i += 1
...
>>> threads = [threading.Thread(target=increment_i) for j in range(10)]
>>> for thread in threads: thread.start()
...
>>> for thread in threads: thread.join()
...
>>> i
4858  # Not 10000

Однако, если у меня есть список l, кажется, что безопасно делать l += [1] для нескольких потоков:

>>> l = []
>>> def extend_l():
...     global l
...     for j in range(1000): l += [1]
...
>>> threads = [threading.Thread(target=extend_l) for j in range(10)]
>>> for thread in threads: thread.start()
...
>>> for thread in threads: thread.join()
...
>>> len(l)
10000

Является l += [1] гарантированным потокобезопасность? Если да, применимо ли это ко всем реализациям Python или просто к CPython?

Изменить: Кажется, что l += [1] является потокобезопасным, но l = l + [1] не...

>>> l = []
>>> def extend_l():
...     global l
...     for j in range(1000): l = l + [1]
...
>>> threads = [threading.Thread(target=extend_l) for j in range(10)]
>>> for thread in threads: thread.start()
...
>>> for thread in threads: thread.join()
...
>>> len(l)
3305  # Not 10000
4b9b3361

Ответ 1

На этот вопрос нет счастливого;-) ответа. Там ничего не гарантируется ни на что, что вы можете подтвердить просто, отметив, что справочное руководство Python не дает никаких гарантий относительно атомарности.

В CPython это вопрос прагматики. Как говорится в статье, посвященной эффботу,

В теории это означает, что для точного учета требуется точное понимание реализации байт-кода PVM [Python Virtual Machine].

И это правда. Специалист CPython знает, что L += [x] является атомарным, потому что они знают все следующее:

  • += компилируется в байт-код INPLACE_ADD.
  • Реализация объекта INPLACE_ADD для объектов списка полностью написана в C (код пути Python не находится на пути выполнения, поэтому GIL не может быть выпущен между байт-кодами).
  • В listobject.c реализация INPLACE_ADD - это функция list_inplace_concat(), и ничто во время ее выполнения не должно выполнять любой код Python пользователя (если это так, GIL может снова быть выпущен).

Это может звучать невероятно сложно, но для кого-то, кто знает об особенностях CPython (в то время, когда он писал эту статью), это действительно так. На самом деле, учитывая ту глубину знания, все это очевидно: -)

Итак, в качестве прагматики эксперты CPython всегда свободно полагались на то, что "операции, которые" выглядят атомными ", действительно должны быть атомарными", а также руководствовались некоторыми языковыми решениями. Например, операция отсутствует в списке effbot (добавлена ​​на язык после того, как он написал эту статью):

x = D.pop(y) # or ...
x = D.pop(y, default)

Один аргумент (в то время) в пользу добавления dict.pop() был именно тем, что очевидная реализация C была бы атомарной, тогда как альтернатива in-use (in the time):

x = D[y]
del D[y]

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

Но документы никогда не говорили, что .pop() был атомарным и никогда не будет. Это нечто вроде "соглашающихся взрослых": если вы достаточно компетентны, чтобы использовать это сознательно, вам не нужна ручная работа. Если вы недостаточно квалифицированы, то применяется последнее предложение статьи effbot:

Если вы сомневаетесь, используйте мьютекс!

В рамках прагматической необходимости основные разработчики никогда не нарушат атомарность примеров effbot (или D.pop() или D.setdefault()) в CPython. Тем не менее, другие реализации вообще не обязаны имитировать эти прагматические варианты. Действительно, поскольку атомарность в этих случаях зависит от конкретной формы байт-кода CPython в сочетании с использованием CPython глобальной блокировки интерпретатора, которая может быть выпущена только между байт-кодами, это может быть реальной болью для других реализаций, имитирующих их.

И вы никогда не знаете: какая-то будущая версия CPython может удалить GIL тоже! Я сомневаюсь, но это теоретически возможно. Но если это произойдет, я уверен, что параллельная версия, поддерживающая GIL, тоже будет сохранена, потому что очень много кода (особенно модулей расширения, написанных в C) также полагается на GIL для безопасности потоков.

Стоит повторять:

Если вы сомневаетесь, используйте мьютекс!

Ответ 2

Из http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm:

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

Следующие операции - все атомарные (L, L1, L2 - списки, D, D1, D2 - dicts, x, y - объекты, i, j - ints):

L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()

Эти arent:

i = i+1
L.append(L[-1])
L[i] = L[j]
D[x] = D[x] + 1

Выше специфицировано только CPython и может варьироваться в разных реализациях Python, таких как PyPy.

Кстати, есть открытая проблема для документирования атомных операций Python - https://bugs.python.org/issue15339