AutoResetEvent и несколько наборов

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

Кажется, что AutoResetEvent ведет себя как семафор. Это правильно? Могу я просто избавиться от Set() в BlockingStack.Get() и покончить с этим? Или это приведет к ситуации, когда я использую только один из элементов стека?

public class BlockingStack
{
    private Stack<MyType> _internalStack;
    private AutoResetEvent _blockUntilAvailable;

    public BlockingStack()
    {
        _internalStack = new Stack<MyType>(5);
        _blockUntilAvailable = new AutoResetEvent(false);

        for (int i = 0; i < 5; ++i)
        {
            var obj = new MyType();
            Add(obj);
        }
    }

    public MyType Get()
    {
        _blockUntilAvailable.WatiOne();

        lock (_internalStack)
        {
            var obj = _internalStack.Pop();
            if (_internalStack.Count > 0)
            {
                _blockUntilAvailable.Set(); // do I need to do this?
            }

            return obj;
        }
    }

    public void Add(MyType obj)
    {
        lock (_internalStack)
        {
            _internalStack.Push(obj);
            _blockUntilAvailable.Set();
        }
    }
}

Мое предположение заключалось в том, что AutoResetEvent сбрасывается для всех ожидающих потоков, когда один из них завершает вызов функции WaitOne(). Тем не менее, кажется, что поступает несколько потоков. Если только я где-то не напутал свою логику.

EDIT: это для Silverlight.


person sohum    schedule 16.12.2011    source источник
comment
Релевантно: stackoverflow.com/questions/3797892 /   -  person Hans Passant    schedule 17.12.2011


Ответы (3)


Вам лучше использовать блокирующую коллекцию, если только вы не пытаетесь понять, как работает многопоточность. Это даст вам блокирующую коллекцию, поддерживаемую стеком:

ConcurrentStack<SomeType> MyStack = new ConcurrentStack<SomeType>();
BlockingCollection<SomeType> SharedStack = new BlockingCollection<SomeType>(MyStack)

Затем вы можете получить к нему доступ потокобезопасным способом со всей блокировкой, выполненной для вас должным образом. См. здесь

Вы можете использовать sharedStack, вызвав sharedStack.Take(), который затем заблокирует получение, пока не будет что-то взять из стека.


Редактировать: мне потребовалось некоторое время (и две попытки), но я думаю, что я решил вашу проблему.

Рассмотрим пустой стек с 3 потоками, ожидающими события.

Add вызывается, в стеке есть один объект, и через событие разрешен один поток.

Немедленно Add вызывается снова.

Теперь первый поток ожидает получения блокировки от Add.

Add добавляет второй объект в стек и позволяет другому потоку пройти через событие.

Теперь два объекта в стеке и 2 потока через событие, оба ожидают блокировки.

Поток First Get теперь блокируется и извлекается. Видит еще один объект в стеке и ВЫЗЫВАЕТ SET.

Третий поток разрешен через событие.

Второй поток Get теперь блокируется и извлекается. Ничего не видит в стеке и не вызывает set.

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

person Russell Troywest    schedule 16.12.2011
comment
Это не отвечает на вопрос ОП: я пытаюсь создать структуру данных вокруг стека, которая блокируется до тех пор, пока в стеке не появится доступный элемент. - person oleksii; 16.12.2011
comment
Нет, не особенно - вот почему я заметил, что он / она может просто хотеть понять, что происходит. Если они на самом деле пытаются создать эту структуру для использования, лучшим вариантом является использование коллекции блокировки (при использовании .net4). Создание блокирующих коллекций сложно сделать правильно и очень легко ошибиться тонкими способами. - person Russell Troywest; 16.12.2011
comment
Это именно то, что делает блокирующая коллекция. Вы вызываете для него Take(), и он блокируется до тех пор, пока не появится что-то доступное для взятия из базовой коллекции. - person Russell Troywest; 16.12.2011
comment
Я должен был упомянуть, что это для Silverlight. Отредактировано. - person sohum; 17.12.2011
comment
@sohum На самом деле это звучит как хорошее решение, вы можете реализовать стек поверх BlockingCollection, используя метод Take. - person oleksii; 17.12.2011
comment
Обидно за Silverlight — BlockingCollection ДЕЙСТВИТЕЛЬНО полезен. На самом деле я просто запустил ваш код после того, как некоторое время смотрел на него и не видел, как вы можете что-то взять, когда стек пуст. У меня работает нормально. Вы получаете исключение? В нынешнем виде вы не сможете получить начальные значения из стека до тех пор, пока что-то не будет добавлено в первый раз, а AutoResetEvents печально известны тем, что не делают того, что вы ожидаете, но похоже, что это должно работать. - person Russell Troywest; 17.12.2011
comment
Да, я получаю исключение, пытаясь извлечь пустой стек. По сути, мой AutoResetEvent сигнализируется, и я блокирую _internalStack, но его счетчик равен 0. Я думал, что это выглядело довольно хорошо, но, видимо, нет, ха-ха. Я предполагаю, что это деталь реализации AutoResetEvent. Жаль, что в SL нет семафора. - person sohum; 17.12.2011
comment
Фу. Понял, что с первого раза не совсем понял. На самом деле мне пришлось встать с постели и все обдумать, потому что это просто продолжало крутиться у меня в голове. Уверен, что на этот раз он у меня есть. Надеюсь на это, действительно не помешало бы немного поспать :) - person Russell Troywest; 17.12.2011

Нет, ваш текущий код не имеет смысла. На данный момент вы блокируете поток каждый раз, когда вызывается метод Get (вызов .WaitOne).

Возможно, вам нужно что-то вроде:

public class BlockingStack<T>
{
    private Stack<T> _internalStack;
    private AutoResetEvent _blockUntilAvailable;

    public BlockingStack()
    {
        _internalStack = new Stack<T>(5);
        _blockUntilAvailable = new AutoResetEvent(false);
    }

    public T Pop()
    {
        lock (_internalStack)
        {
            if (_internalStack.Count == 0)
                _blockUntilAvailable.WaitOne();

            return _internalStack.Pop();
        }
    }

    public void Push(T obj)
    {
        lock (_internalStack)
        {
            _internalStack.Push(obj);

            if(_internalStack.Count == 0)
                _blockUntilAvailable.Set();
        }
    }
}

Идея в том, что если текущее количество элементов в _internalStack равно 0, то он должен ждать сигнала от метода Push. Получив сигнал, он движется дальше и извлекает элемент из стека.


EDIT: в приведенном выше коде есть две проблемы:

  1. Всякий раз, когда Pop блокируется с .WaitOne, он не освобождает блокировку _internalStack, и поэтому Push никогда не сможет получить блокировку.

  2. Когда Pop вызывается несколько раз в одном и том же потоке, они имеют одно и то же начальное состояние для AutoResetEvent — например. Push сигнализирует AutoResetEvent о добавлении элемента. Теперь, когда я извлекаю элемент, он работает нормально в первый раз, так как на самом деле элемент находится в Stack. Однако во второй раз в Stack нет значения, поэтому он ожидает, вызвав .WaitOne в AutoResetEvent, но, поскольку вызов Push сигнализировал об этом событии, он просто вернет true, а не будет ждать, как ожидалось.

(Рабочая) альтернатива:

public class BlockingStack<T>
{
    private Stack<T> _internalStack;

    public BlockingStack()
    {
        _internalStack = new Stack<T>(5);
    }

    public T Pop()
    {
        lock (_internalStack)
        {
            if (_internalStack.Count == 0)
                Monitor.Wait(_internalStack);

            return _internalStack.Pop();
        }
    }

    public void Push(T obj)
    {
        lock (_internalStack)
        {
            _internalStack.Push(obj);
            Monitor.Pulse(_internalStack);
        }
    }
}
person ebb    schedule 18.12.2011
comment
Не работает, вызовите Push, Pop, Pop, Push и наблюдайте тупик. - person Hans Passant; 18.12.2011
comment
@HansPassant, зачем вам вызывать Push, Pop, Pop, Push в одном потоке? Вы ожидаете, что он каким-то образом вызовет Push в том же потоке, хотя Pop блокирует поток? - person ebb; 18.12.2011
comment
@HansPassant, пожалуйста, посмотрите обновление и посмотрите, правильно ли я понял на этот раз :) - person ebb; 18.12.2011
comment
Обратите внимание на цикл while в сообщении, на которое я ссылаюсь, он необходим, когда существует более одного потока потребителей. - person Hans Passant; 18.12.2011
comment
@HansPassant, я не уверен, зачем тебе цикл while ... Не могли бы вы уточнить? - person ebb; 18.12.2011

Я не проверял решение на основе Monitor, но написал решение на основе семафора, которое, похоже, работает:

public class Semaphore
{
    private int _count;
    private int _maximum;
    private object _countGuard;

    public Semaphore(int maximum)
    {
        _count = 0;
        _maximum = maximum;
        _countGuard = new object();
    }

    public void WaitOne()
    {
        while (true)
        {
            lock (_countGuard)
            {
                if (_count < _maximum)
                {
                    _count++;
                    return;
                }
            }
            Thread.Sleep(50);
        }
    }

    public void ReleaseOne()
    {
        lock (_countGuard)
        {
            if (_count > 0)
            {
                _count--;
            }
        }
    }
}

public class BlockingStack
{
    private Stack<MyType> _internalStack;
    private Semaphore _blockUntilAvailable;

    public BlockingStack()
    {
        _internalStack = new Stack<MyType>(5);
        _blockUntilAvailable = new Semaphore(5);

        for (int i = 0; i < 5; ++i)
        {
            var obj = new MyType();
            Add(obj);
        }
    }

    public MyType Get()
    {
        _blockUntilAvailable.WaitOne();

        lock (_internalStack)
        {
            var obj = _internalStack.Pop();
            return obj;
        }
    }

    public void Add(MyType obj)
    {
        lock (_internalStack)
        {
            _internalStack.Push(obj);
            _blockUntilAvailable.ReleaseOne();
        }
    }
}
person sohum    schedule 30.12.2011