thread safe concurrentqueue concurrentbag concurrent c# .net multithreading queue

safe - concurrentbag c#



Problemas de la cola de.NET multiproceso (3)

Tengo un error extraño en mi código. Es extremadamente raro (tal vez una vez cada pocas semanas), pero está ahí y no estoy seguro de por qué.

Tenemos 2 subprocesos en ejecución, 1 subproceso obtiene mensajes en red y los agrega a una Cola como esta:

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

Otro hilo quita los mensajes de esta cola y los maneja, así:

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

Sin embargo, de vez en cuando recibo una NullReferenceException en el switch (message.messageType) línea switch (message.messageType) y puedo ver en el depurador que el mensaje es nulo.

No es posible que se haya colocado un valor nulo en la cola (consulte el primer bit del código), y estas son las únicas 2 cosas que utilizan la cola.

¿La cola no es segura para los subprocesos? ¿Podría ser que estoy eliminando en cola en el momento exacto en que el otro subproceso está en cola y esto provoca la falla?


¿La cola no es segura para los subprocesos? ¿Podría ser que estoy eliminando en cola en el momento exacto en que el otro subproceso está en cola y esto provoca la falla?

Exactamente. Queue no es segura para subprocesos. Una cola segura para subprocesos es System.Collections.Concurrent.ConcurrentQueue . Úsalo en su lugar para solucionar tu problema.


En caso de que esté interesado en la razón exacta:

Enqueue ve así:

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

Y Dequeue así:

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

La carrera es así:

  • Hay 1 elemento en una cola (cabecera == cola), por lo que el hilo del lector comienza a retirarse de la cola pero se interrumpe después de la primera línea en la Dequeue
  • Luego, otro elemento se pone en tail y se coloca en la tail posición que es igual a la head en este punto.
  • Ahora Dequeue reanuda y sobrescribe el elemento que acaba de insertar Enqueue con el default(T)
  • La próxima vez que llame a la salida de la cola, obtendrá el valor predeterminado (T) (en su caso nulo) en lugar del valor real

while (NetworkingClient.DataMessages.Count > 0) { // once every two weeks a context switch happens to be here. DataMessage message = NetworkingClient.DataMessages.Dequeue(); switch (message.messageType) { ... } }

... y cuando obtiene ese cambio de contexto en esa ubicación, el resultado de la primera expresión ( NetworkingClient.DataMessages.Count > 0 ) es verdadero para ambos subprocesos, y el que obtiene la operación Dequeue() primero obtiene el objeto y el segundo subproceso es nulo (en lugar de InvalidOperationException porque el estado interno de la cola no se actualizó completamente para lanzar la excepción correcta).

Ahora, tú tienes dos opciones:

  1. Utilice el .NET 4.0 ConcurrentQueue

  2. Refactoriza tu código:

y hacer que se vea de alguna manera así:

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

Edición: actualizado para reflejar el comentario de Heandel.