Очереди и дескрипторы ожидания в C # - PullRequest
5 голосов
/ 30 апреля 2010

У меня в приложении несколько лет был следующий код, и я никогда не видел проблемы с ним.

while ((PendingOrders.Count > 0) || (WaitHandle.WaitAny(CommandEventArr) != 1))
{
    lock (PendingOrders)
    {
       if (PendingOrders.Count > 0)
       {
           fbo = PendingOrders.Dequeue();
       }
       else
       {
           fbo = null;
       }
    }

    // Do Some Work if fbo is != null
}

Где CommandEventArr состоит из NewOrderEvent (событие автоматического сброса) и ExitEvent (событие ручного сброса).

Но я не уверен, является ли это потокобезопасным (если предположить, что N потоков производителя блокируют очередь перед постановкой в ​​очередь и один потребительский поток, который выполняет код выше). Кроме того, мы можем предположить, что свойство Queue.Count просто возвращает значение одного экземпляра Int32 из класса Queue (без энергозависимой или блокируемой или блокировки и т. Д.).

Какой обычный шаблон используется с очередью и AutoResetEvent, чтобы исправить это и сделать то, что я пытаюсь сделать с кодом выше?

(Отредактировано, чтобы немного изменить вопрос после того, как было правильно указано, что Queue.Count может делать все что угодно и его конкретную реализацию).

Ответы [ 4 ]

4 голосов
/ 30 апреля 2010

Выглядит довольно поточно-ориентированным для меня, WaitAny () просто завершится немедленно, потому что событие уже установлено. Это не проблема.

Не нарушайте синхронизацию потоков, которая работает. Но если вам нужна лучшая мышеловка, вы можете рассмотреть BlockingQueue Джо Даффи в этой журнальной статье . Более общая версия доступна в .NET 4.0, System.Collections.Concurrent.BlockingCollection с ConcurrentQueue в качестве практической реализации.

3 голосов
/ 30 апреля 2010

Вы правы. Код не является потокобезопасным. Но не по той причине, вы думаете.

AutoResetEvent в порядке. Но только потому, что вы приобрели блокировку и повторно протестировали PendingOrders.Count. Суть в том, что вы вызываете PendingOrders.Count за пределами блокировки. Поскольку класс Queue не является потокобезопасным, ваш код не является потокобезопасным ... точка.

Теперь в действительности у вас, вероятно, никогда не будет проблем с этим по двум причинам. Во-первых, свойство Queue.Count почти наверняка разработано так, чтобы никогда не оставлять объект в полусгоревшем состоянии. В конце концов, он, вероятно, просто вернет переменную экземпляра. Во-вторых, отсутствие барьера памяти для этого чтения не окажет существенного влияния в более широком контексте вашего кода. Худшее, что может случиться, это то, что вы получите устаревшее чтение на одной итерации цикла, а затем полученная блокировка неявно создаст барьер памяти, и на следующей итерации будет выполнено новое чтение. Я предполагаю здесь, что есть только один поток элементов очереди. Вещи значительно меняются, если их 2 или более.

Однако позвольте мне сделать это совершенно ясно. У вас нет гарантии, что PendingOrders.Count не изменит состояние объекта во время его выполнения . И поскольку он не заключен в блокировку, другой поток может инициировать на нем операцию, пока он еще находится в этом полуобеспеченном состоянии.

2 голосов
/ 30 апреля 2010

Использование ручных событий ...

ManualResetEvent[] CommandEventArr = new ManualResetEvent[] { NewOrderEvent, ExitEvent };

while ((WaitHandle.WaitAny(CommandEventArr) != 1))
{
    lock (PendingOrders)
    {
        if (PendingOrders.Count > 0)
        {
            fbo = PendingOrders.Dequeue();
        }
        else
        {
            fbo = null;
            NewOrderEvent.Reset();
        }
    }
}

Тогда вам также необходимо обеспечить блокировку на стороне очереди:

    lock (PendingOrders)
    {
        PendingOrders.Enqueue(obj);
        NewOrderEvent.Set();
    }
0 голосов
/ 30 апреля 2010

Для этого следует использовать только WaitAny и гарантировать, что он будет сигнализироваться при каждом новом заказе, добавленном в коллекцию PendingOrders:

while (WaitHandle.WaitAny(CommandEventArr) != 1))
{
   lock (PendingOrders)
   {
      if (PendingOrders.Count > 0)
      {
          fbo = PendingOrders.Dequeue();
      }
      else
      {
          fbo = null;

          //Only if you want to exit when there are no more PendingOrders
          return;
      }
   }

   // Do Some Work if fbo is != null
}
...