Многопоточные проблемы очереди .NET - PullRequest
16 голосов
/ 18 июня 2011

У меня странная ошибка в моем коде. Это крайне редко (возможно, случается раз в несколько недель), но оно есть, и я не знаю, почему.

У нас работает 2 потока, 1 поток получает сетевые сообщения и добавляет их в очередь следующим образом:

DataMessages.Enqueue(new DataMessage(client, msg));

Другой поток забирает сообщения из этой очереди и обрабатывает их, например:

while (NetworkingClient.DataMessages.Count > 0)
{
    DataMessage message = NetworkingClient.DataMessages.Dequeue();

    switch (message.messageType)
    {
       ...
    }
}

Однако, однажды, очень часто я получаю исключение NullReferenceException в строке switch (message.messageType), и я вижу в отладчике, что сообщение пустое.

Невозможно, чтобы в очередь было помещено нулевое значение (см. Первый бит кода), и это единственные 2 вещи, которые используют очередь.

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

Ответы [ 3 ]

11 голосов
/ 18 июня 2011

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

Точно.Queue не является потокобезопасным.Потокобезопасная очередь - System.Collections.Concurrent.ConcurrentQueue.Используйте это вместо того, чтобы исправить вашу проблему.

9 голосов
/ 18 июня 2011
    while (NetworkingClient.DataMessages.Count > 0)
    {
        // once every two weeks a context switch happens to be here.
        DataMessage message = NetworkingClient.DataMessages.Dequeue();

        switch (message.messageType)
        {
           ...
        }
    }

... и когда вы получите этот переключатель контекста в этом месте, результат первого выражения (NetworkingClient.DataMessages.Count > 0) будет истинным для обоих потоков, и тот, который получает операцию Dequeue(), сначала получаетобъект и второй поток получают нулевое значение (вместо InvalidOperationException, поскольку внутреннее состояние очереди не было полностью обновлено, чтобы вызвать правильное исключение).

Теперь у вас есть две опции:

  1. Используйте .NET 4.0 ConcurrentQueue

  2. Измените код:

и сделайте его похожим наthis:

while(true)
{
  DataMessage message = null;

  lock(NetworkingClient.DataMessages.SyncRoot) {
       if(NetworkingClient.DataMessages.Count > 0) {
          message = NetworkingClient.DataMessages.Dequeue();
       } else {
         break;
       }
    }
    // .. rest of your code
}

Edit: обновлено, чтобы отразить комментарий Heandel.

7 голосов
/ 19 июня 2011

Если вас интересует точная причина:

Enqueue выглядит так:

this._array[this._tail] = item;
this._tail = (this._tail + 1) % this._array.Length;
this._size++;
this._version++;

И Dequeue вот так:

T result = this._array[this._head];
this._array[this._head] = default(T);
this._head = (this._head + 1) % this._array.Length;
this._size--;
this._version++;

Гонка идет следующим образом:

  • В очереди 1 элемент (голова == хвост), поэтому ваш поток чтения начинает отключаться, но прерывается после первой строки в Dequeue
  • Затем другой элемент ставится в очередь и помещается в позицию tail, которая в данный момент равна head.
  • Теперь Dequeue возобновляет и перезаписывает элемент, который был только что вставлен Enqueue с помощью default(T)
  • При следующем вызове dequeue вы получите значение по умолчанию (T) (в вашем случае ноль) вместо действительного значения
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...