Я заметил, что часто предлагается использовать очереди с несколькими потоками вместо списков и .pop()
. Это потому, что списки не являются потокобезопасными или по какой-то другой причине?
Я заметил, что часто предлагается использовать очереди с несколькими потоками вместо списков и .pop()
. Это потому, что списки не являются потокобезопасными или по какой-то другой причине?
Ответы:
Сами списки являются потокобезопасными. В CPython GIL защищает от одновременного доступа к ним, а другие реализации стараются использовать детализированную блокировку или синхронизированный тип данных для своих реализаций списка. Однако, хотя сами списки не могут испортиться при попытках одновременного доступа, данные списков не защищены. Например:
L[0] += 1
не гарантируется фактическое увеличение L [0] на единицу, если другой поток делает то же самое, потому что +=
это не атомарная операция. (Очень, очень мало операций в Python на самом деле являются атомарными, потому что большинство из них может вызвать вызов произвольного кода Python.) Вам следует использовать очереди, потому что, если вы просто используете незащищенный список, вы можете получить или удалить неправильный элемент из-за гонки. условия.
Чтобы прояснить вопрос в превосходном ответе Томаса, следует упомянуть, что он append()
является потокобезопасным.
Это связано с тем, что не нужно беспокоиться о том, что читаемые данные будут в одном и том же месте, как только мы перейдем к записи в них. append()
Операция не читает данные, он только записывает данные в список.
PyList_Append
выполняется в одном GIL-замке. Дается ссылка на объект для добавления. Содержимое этого объекта может быть изменено после его оценки и перед выполнением вызова PyList_Append
. Но это все равно будет тот же объект, и он будет безопасно добавлен (если вы это сделаете lst.append(x); ok = lst[-1] is x
, то ok
, конечно , может быть ложным). Код, на который вы ссылаетесь, не читает из добавленного объекта, кроме как для его INCREF. Он читает и может перераспределять список, к которому добавляется.
L[0] += x
будет выполнять __getitem__
включение, L
а затем __setitem__
включение L
- если L
поддерживает, то __iadd__
это будет немного по-другому работать на объектном интерфейсе, но L
на уровне интерпретатора python по- прежнему выполняются две отдельные операции (вы увидите их в скомпилированный байт-код). Это append
делается одним вызовом метода в байт-коде.
remove
?
Вот всеобъемлющий еще не исчерпывающий перечень примеров из list
операций и действительно ли они являются поточно. Надеемся получить ответ относительно obj in a_list
языковой конструкции здесь .
У меня недавно был этот случай, когда мне нужно было непрерывно добавлять в список в одном потоке, циклически проходить по элементам и проверять, готов ли элемент, в моем случае это был AsyncResult и удалять его из списка, только если он был готов. Я не смог найти ни одного примера, который бы четко продемонстрировал мою проблему. Вот пример, демонстрирующий непрерывное добавление в список в одном потоке и непрерывное удаление из этого же списка в другом потоке. Дефектная версия легко работает на меньших числах, но при этом сохраняет достаточно большие числа и запускает несколько раз, и вы увидите ошибку
FLAWED версия
import threading
import time
# Change this number as you please, bigger numbers will get the error quickly
count = 1000
l = []
def add():
for i in range(count):
l.append(i)
time.sleep(0.0001)
def remove():
for i in range(count):
l.remove(i)
time.sleep(0.0001)
t1 = threading.Thread(target=add)
t2 = threading.Thread(target=remove)
t1.start()
t2.start()
t1.join()
t2.join()
print(l)
Вывод при ОШИБКЕ
Exception in thread Thread-63:
Traceback (most recent call last):
File "/Users/zup/.pyenv/versions/3.6.8/lib/python3.6/threading.py", line 916, in _bootstrap_inner
self.run()
File "/Users/zup/.pyenv/versions/3.6.8/lib/python3.6/threading.py", line 864, in run
self._target(*self._args, **self._kwargs)
File "<ipython-input-30-ecfbac1c776f>", line 13, in remove
l.remove(i)
ValueError: list.remove(x): x not in list
Версия, которая использует блокировки
import threading
import time
count = 1000
l = []
lock = threading.RLock()
def add():
with lock:
for i in range(count):
l.append(i)
time.sleep(0.0001)
def remove():
with lock:
for i in range(count):
l.remove(i)
time.sleep(0.0001)
t1 = threading.Thread(target=add)
t2 = threading.Thread(target=remove)
t1.start()
t2.start()
t1.join()
t2.join()
print(l)
Вывод
[] # Empty list
Вывод
Как упоминалось в предыдущих ответах, в то время как процесс добавления или выталкивания элементов из самого списка является потокобезопасным, не является безопасным поток, когда вы добавляете в один поток и вставляете в другой
with r:
) вместо явного вызова r.acquire()
иr.release()