Является ли список (list) в Python потокобезопасным?

Ответ

В Python список (list) не является потокобезопасным. Это означает, что если несколько потоков одновременно изменяют список (например, добавляют или удаляют элементы), это может привести к состоянию гонки (race condition), повреждению данных или неожиданному поведению. Это происходит потому, что операции со списком не являются атомарными.

Пример проблемы (состояние гонки):

import threading

shared_list = []

def add_items():
    for i in range(1000):
        shared_list.append(i)

threads = []
for _ in range(10):
    t = threading.Thread(target=add_items)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

# Ожидаемый размер: 10 * 1000 = 10000
# Фактический размер может быть меньше из-за состояний гонки
print(len(shared_list))

Решение для потокобезопасной работы:

  1. Использование threading.Lock: Это самый распространенный способ синхронизации доступа к общим ресурсам. Блокировка гарантирует, что только один поток может выполнять критическую секцию кода в любой момент времени.

    import threading
    
    lock = threading.Lock()
    shared_list = []
    
    def add_items_safe():
        for i in range(1000):
            with lock: # Захватываем блокировку перед изменением списка
                shared_list.append(i)
    
    threads = []
    for _ in range(10):
        t = threading.Thread(target=add_items_safe)
        threads.append(t)
        t.start()
    
    for t in threads:
        t.join()
    
    print(len(shared_list)) # Будет 10000
  2. Использование queue.Queue: Если требуется потокобезопасная структура данных с логикой FIFO (первым пришел — первым ушел), queue.Queue из модуля queue является отличным выбором, так как он изначально разработан для потокобезопасного обмена данными между потоками.

  3. Для межпроцессного взаимодействия: Для обмена данными между процессами (не потоками) можно использовать multiprocessing.Manager().list() или другие средства синхронизации из модуля multiprocessing.

Ответ 18+ 🔞

А, слушай, смотри, вот эта вся история с потоками и списками в питоне — это вообще отдельный цирк с конями, блядь. Представь себе: у тебя есть список, обычный такой, list, вроде всё мирно, да? А потом ты запускаешь на него десять потоков, и они все как с цепи сорвутся — начинают туда пихать свои элементы одновременно. И тут, сука, начинается такое...

Это ж как на толкучке, когда все лезут в одну сумку, а в итоге половина вещей мимо падает, и в сумке нихуя не остаётся, понимаешь? Вот и тут так же. Ожидаешь ты, что десять потоков по тысяче элементов каждый — это десять тысяч, блядь, а на выходе получаешь, например, девять с половиной, а то и меньше. Потому что операции-то не атомарные, ёпта! Они там друг другу нахуй наступают, перезаписывают индексы, и в итоге — пиздец, данные похерились.

Вот смотри, какой пиздец может выйти:

import threading

shared_list = []

def add_items():
    for i in range(1000):
        shared_list.append(i)

threads = []
for _ in range(10):
    t = threading.Thread(target=add_items)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

# Ожидаемый размер: 10 * 1000 = 10000
# Фактический размер может быть меньше из-за состояний гонки
print(len(shared_list))

Запустишь это — и волнение ебать, с каждым разом разный результат, как в лотерее, блядь! То девять тысяч, то восемь, а то и вообще какая-нибудь дичь.

Так что же делать, спросишь ты? А выход есть, не ссы!

Первый и самый главный способ — threading.Lock. Это как такой здоровенный мужик с дубиной у входа в список стоит. Хочешь зайти что-то добавить или удалить — получи, блядь, разрешение. Один вошёл — все остальные ждут, пока он не выйдет. Порядок, сука, железный!

import threading

lock = threading.Lock()
shared_list = []

def add_items_safe():
    for i in range(1000):
        with lock: # Захватываем блокировку перед изменением списка
            shared_list.append(i)

threads = []
for _ in range(10):
    t = threading.Thread(target=add_items_safe)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(len(shared_list)) # Будет 10000

Вот теперь, сука, всё чётко. Все десять тысяч на месте, как штыки. Блокировка — наше всё, ебать мои старые костыли.

Второй вариант — queue.Queue. Это если тебе нужна не просто куча данных, а чтобы они ещё и в порядке приходили — кто первый пришёл, тот первый и ушёл. Эта штука из коробки потокобезопасная, там внутри всё уже прикручено. Просто бери и пользуйся, не еби мозг.

Ну и если ты совсем упоротый и работаешь с процессами, а не потоками (это, кстати, другая песня, там память отдельная у каждого), то тут уже multiprocessing.Manager().list() в помощь. Но это уже, как говорится, совсем другая история, в рот меня чих-пых.

Короче, суть в чём: со списками в потоках шутить не надо, а то получишь не данные, а сплошное расстройство и недосчёт. Либо блокировку ставь, либо очередь используй — и спи спокойно.