Чи списки безпечні для потоків?


155

Я зауважую, що часто пропонується використовувати черги з декількома потоками замість списків та .pop(). Це тому, що списки не є безпечними для потоків чи з якоїсь іншої причини?


1
Важко завжди сказати, що саме гарантовано безпечно для потоків у Python, і важко міркувати про безпеку потоку в ньому. Навіть дуже популярний біткойн-гаманець Electrum мав помилки одночасності.
судо

Відповіді:


182

Самі списки є безпечними для ниток. У CPython GIL захищає від одночасного доступу до них, а інші реалізації дбають про використання дрібнозернистого блокування або синхронізованого типу даних для їх реалізації списків. Однак, в той час як списки сам не може йти ксебе спроб отримати одночасний доступ, списки ігрових дані не захищені. Наприклад:

L[0] += 1

не гарантується фактично збільшити L [0] на одну, якщо інша нитка зробить те саме, оскільки +=це не атомна операція. (Дуже мало операцій в Python насправді є атомними, оскільки більшість з них може викликати виклик довільного коду Python.) Ви повинні використовувати черги, оскільки якщо ви просто використовуєте незахищений список, ви можете отримати або видалити неправильний елемент через гонку умови.


1
Чи deque також безпечний для ниток? Здається більш доцільним для мого використання.
lemiant

20
Усі об'єкти Python мають однакову безпеку для потоків - вони самі не пошкоджуються, але їх дані можуть бути. collection.deque - це те, що стоїть за об’єктами Queue.Queue. Якщо ви отримуєте доступ до речей з двох потоків, вам дійсно слід використовувати об’єкти Queue.Queue. Дійсно.
Thomas Wouters

10
lemiant, deque є безпечним для ниток. Із розділу 2 Fluent Python: "Клас collection.deque - безпечна для потоків двокімнатна черга, призначена для швидкого вставлення та видалення з обох кінців. [...] Операції з додаванням та поплефтом є атомними, тому deque безпечний для використовувати як чергу LIFO у багатопотокових програмах без необхідності використання замків. "
Al Sweigart

3
Це відповідь про CPython чи про Python? Яка відповідь для самого Python?
користувач541686

@Nils: Ну, перша сторінка , яку ви пов'язані говорить Python замість CPython , тому що це опис мови Python. І це друге посилання буквально говорить про те, що існує кілька реалізацій мови Python, лише одна, яка, можливо, є більш популярною. Зважаючи на те, що питання стосувалося Python, у відповіді має бути описано, що може гарантуватися у будь-якій відповідній реалізації Python, а не тільки в CPython, зокрема.
користувач541686

89

Щоб уточнити кращу відповідь Томаса, слід зазначити, що append() це безпечно для ниток.

Це тому, що немає ніякого занепокоєння, що дані, які читаються, будуть там же, коли ми підемо в них записувати . append()Операція не читає дані, він тільки записує дані в список.


1
PyList_Append - це читання з пам'яті. Ви маєте на увазі, що його читання та запис відбувається у тому самому блоці GIL? github.com/python/cpython/blob/…
amwinter

1
@amwinter Так, весь виклик PyList_Appendробиться в одному блоці GIL. Дається посилання на об'єкт для додавання. Вміст цього об'єкта може бути змінено після його оцінювання та перед тим, як здійснити виклик PyList_Append. Але це все одно буде той самий об’єкт і надійно доданий (якщо ви це зробите lst.append(x); ok = lst[-1] is x, то ok, звичайно , може бути помилковим). Код, на який ви посилаєтесь, не зчитується з доданого об'єкта, за винятком того, що він НЕ ВКЛЮЧИТЬ його. Він читає та може перерозподілити список, до якого додається.
greggo

3
Точка dotancohen «сек, що L[0] += xвиконає __getitem__на , Lа потім __setitem__на L- якщо Lпідтримує __iadd__це буде робити речі трохи по- іншому на кордоні об'єкта, але є ще дві операції на Lна рівні пітон інтерпретатора (ви будете бачити їх у складений байт-код). appendРобиться в аа один виклик методу в байткод.
greggo

6
Як щодо remove?
акріз

2
прихильне! тож чи можу я додавати в одну нитку постійно та вставляти в іншу нитку?
PirateApp


2

Нещодавно у мене був такий випадок, коли мені потрібно було додавати до списку в один потік, прокручувати елементи та перевіряти, чи готовий елемент, у моєму випадку це AsyncResult і видаляти його зі списку лише в тому випадку, якщо він готовий. Я не міг знайти жодних прикладів, які наочно продемонстрували мою проблему. Ось приклад демонстрації додавання до списку в одному потоці безперервного та видалення з того самого списку в іншому потоці безперервно. Недосконала версія легко працює на менших числах, але зберігає числа досить великими і виконує кілька разів, і ви побачите помилку

ВИМОГА версія

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

Висновок

Як було сказано в попередніх відповідях, хоча акт додавання або вискакування елементів із самого списку безпечний для потоків, те, що не є безпечним для потоків, - це коли ви додаєте в одну нитку, а іншу -


6
Версія з замками має таку саму поведінку, що і версія без замків. По суті, помилка виникає через те, що вона намагається видалити те, чого немає в списку, це не має нічого спільного з безпекою потоку. Спробуйте запустити версію з блокуваннями після зміни порядку запуску, тобто запустіть t2 перед t1, і ви побачите ту саму помилку. щоразу, коли t2 випереджає t1, помилка буде виникати незалежно від того, використовуєте ви блокування чи ні.
Dev

1
Крім того, вам краще використовувати контекстний менеджер ( with r:), а не чітко зателефонувати r.acquire()таr.release()
GordonAitchJay

1
@GordonAitchJay 👍
Тимофі К. Квін
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.