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 latail
posición que es igual a lahead
en este punto. - Ahora
Dequeue
reanuda y sobrescribe el elemento que acaba de insertarEnqueue
con eldefault(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:
Utilice el .NET 4.0 ConcurrentQueue
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.