Является ли коллекция Stack потокобезопасной?

Ответ

Нет, стандартный System.Collections.Generic.Stack<T> не является потокобезопасным. Его методы не защищены от состояния гонки при одновременном вызове из нескольких потоков, что может привести к повреждению внутренней структуры данных, потере элементов или исключениям.

Способы обеспечения потокобезопасности:

  1. Явная синхронизация с помощью lock: Самый простой способ для контролируемого доступа.

    private Stack<int> _stack = new Stack<int>();
    private readonly object _syncLock = new object();
    
    public void PushSafely(int item)
    {
        lock (_syncLock)
        {
            _stack.Push(item);
        }
    }
    
    public bool TryPopSafely(out int result)
    {
        lock (_syncLock)
        {
            if (_stack.Count > 0)
            {
                result = _stack.Pop();
                return true;
            }
            result = default;
            return false;
        }
    }
  2. Использование ConcurrentStack<T>: Специализированная потокобезопасная коллекция из пространства имён System.Collections.Concurrent. Она использует эффективные lock-free алгоритмы для отдельных операций Push и TryPop.

    using System.Collections.Concurrent;
    
    ConcurrentStack<int> concurrentStack = new ConcurrentStack<int>();
    
    // Потокобезопасные операции
    concurrentStack.Push(10);
    concurrentStack.Push(20);
    
    if (concurrentStack.TryPop(out int poppedItem))
    {
        Console.WriteLine("Popped: " + poppedItem); // Может быть 20 или 10
    }

    Важно: Хотя ConcurrentStack гарантирует атомарность отдельных операций, последовательность вызовов (например, if(stack.Count>0) stack.Pop()) по-прежнему требует внешней синхронизации, так как между проверкой и действием состояние может измениться другим потоком. Для таких сценариев используйте TryPop.

Ответ 18+ 🔞

Да ты что, серьёзно? Ну блядь, классический Stack<T> — это же просто банка с печеньем, которую на всеобщее обозрение поставили. Любой поток может туда руку сунуть, когда захочет, и будет тебе, сука, состояние гонки, как на распродаже в Ашане. Данные повредятся, элементы потеряются, исключения полетят — пиздец, короче.

Как с этим жить, если потоки лезут со всех щелей:

  1. Взять в замок lock: Самый понятный способ, если ты контролируешь, кто и когда лезет в эту кучу.

    private Stack<int> _stack = new Stack<int>();
    private readonly object _syncLock = new object(); // Вот этот твой палочник-выручалочник
    
    public void PushSafely(int item)
    {
        lock (_syncLock) // Закрылся на клюшку — и никто не лезет
        {
            _stack.Push(item);
        }
    }
    
    public bool TryPopSafely(out int result)
    {
        lock (_syncLock)
        {
            if (_stack.Count > 0)
            {
                result = _stack.Pop();
                return true;
            }
            result = default;
            return false;
        }
    }
  2. Взять ConcurrentStack<T>: А это, блядь, уже специально заточенная под многопоточку банка с хитрым механизмом. Там внутри не просто замок, а lock-free алгоритмы, чтоб не тормозить почём зря.

    using System.Collections.Concurrent;
    
    ConcurrentStack<int> concurrentStack = new ConcurrentStack<int>();
    
    // Толкай и вытаскивай из разных потоков — не обосрёшься
    concurrentStack.Push(10);
    concurrentStack.Push(20);
    
    if (concurrentStack.TryPop(out int poppedItem)) // Главное — используй TryPop, а не Pop
    {
        Console.WriteLine("Popped: " + poppedItem); // Может выскочить 20, а может и 10 — как повезёт
    }

    Вот тут внимание, ёпта: Хотя каждая операция Push и TryPop сама по себе атомарна, если ты сделаешь по-дурацки: if(stack.Count>0) stack.Pop() — то между проверкой и действием другой поток может всё испортить. Поэтому только TryPop, блядь, только хардкор! Для сложных сценариев — внешняя синхронизация.