c# .net multithreading interlocked interlocked-increment

c# - Leyendo un int que es actualizado por Interlocked en otros hilos



.net multithreading (4)

(Esto es una repetición de: ¿Cómo leer correctamente un campo int Interlocked.Increment''ed? Pero, después de leer las respuestas y los comentarios, todavía no estoy seguro de cuál es la respuesta correcta).

Hay un código que no poseo y no puedo cambiar para usar bloqueos que incrementa un contador int (numberOfUpdates) en varios hilos diferentes. Todas las llamadas utilizan:

Interlocked.Increment(ref numberOfUpdates);

Quiero leer numberOfUpdates en mi código. Ahora, ya que esto es un int, sé que no se puede romper. Pero, ¿cuál es la mejor manera de garantizar que obtenga el último valor posible? Parece que mis opciones son:

int localNumberOfUpdates = Interlocked.CompareExchange(ref numberOfUpdates, 0, 0);

O

int localNumberOfUpdates = Thread.VolatileRead(numberOfUpdates);

¿Funcionarán ambos (en el sentido de entregar el último valor posible, independientemente de las optimizaciones, reordenamientos, almacenamiento en caché, etc.)? ¿Se prefiere una sobre la otra? ¿Hay una tercera opción que sea mejor?


¿Funcionarán ambos (en el sentido de entregar el último valor posible, independientemente de las optimizaciones, reordenamientos, almacenamiento en caché, etc.)?

No, el valor que obtienes siempre es obsoleto. Qué tan antiguo puede ser el valor es completamente impredecible. La gran mayoría de las veces estará obsoleto unos pocos nanosegundos, más o menos, dependiendo de la rapidez con la que actúe sobre el valor. Pero no hay un límite superior razonable:

  • su hilo puede perder el procesador cuando cambia de contexto a otro hilo en el núcleo. Los retrasos típicos son de alrededor de 45 ms sin un límite superior firme. Esto no significa que otro subproceso en su proceso también se desconecte, puede seguir funcionando y continuar mutando el valor.
  • Al igual que con cualquier código de modo de usuario, su código también está sujeto a fallas de página. Incurre cuando el procesador necesita RAM para otro proceso. En una máquina muy cargada que puede y saldrá el código activo de la página. Como sucede a veces con el código del controlador del mouse, por ejemplo, dejando un cursor de mouse congelado.
  • Los subprocesos gestionados están sujetos a pausas de recolección de basura casi aleatorias. Tiende a ser el problema menor, ya que es probable que también se detenga otro subproceso que está mutando el valor.

Cualquier cosa que hagas con el valor debe tener esto en cuenta. No hace falta decir que es muy difícil. Los ejemplos prácticos son difíciles de encontrar. .NET Framework es una gran parte del código marcado por la batalla. Puede ver la referencia cruzada al uso de VolatileRead desde la Fuente de referencia . Número de hits: 0.


Bueno, cualquier valor que lea siempre será un tanto obsoleto, como dijo Hans Passant. Solo puede controlar una garantía de que otros valores compartidos sean consistentes con el que acaba de leer utilizando cercas de memoria en el medio de la lectura del código de varios valores compartidos sin bloqueos (es decir, están en el mismo grado de "rigidez")

Las vallas también tienen el efecto de derrotar algunas optimizaciones del compilador y reordenarlas, lo que evita comportamientos inesperados en el modo de lanzamiento en diferentes plataformas.

Thread.VolatileRead hará que se emita una valla de memoria completa para que no se puedan reordenar las lecturas o escrituras en torno a su lectura del int (en el método que lo está leyendo). Obviamente, si solo estás leyendo un único valor compartido (y no estás leyendo otra cosa compartida y el orden y la coherencia de ambos son importantes), entonces puede que no parezca necesario ...

Pero creo que lo necesitarás de todos modos para vencer algunas optimizaciones del compilador o la CPU para que no obtengas la lectura más "obsoleta" de lo necesario.

Un maniquí Interlocked.CompareExchange hará lo mismo que Thread.VolatileRead (comportamiento de derrota de la valla y optimización completa).

Hay un patrón que se sigue en el marco utilizado por CancelTokenSource http://referencesource.microsoft.com/#mscorlib/system/threading/CancellationTokenSource.cs#64

//m_state uses the pattern "volatile int32 reads, with cmpxch writes" which is safe for updates and cannot suffer torn reads. private volatile int m_state; public bool IsCancellationRequested { get { return m_state >= NOTIFYING; } } // .... if (Interlocked.CompareExchange(ref m_state, NOTIFYING, NOT_CANCELED) == NOT_CANCELED) { } // ....

La palabra clave volátil tiene el efecto de emitir una "media" valla. (es decir, bloquea que las lecturas / escrituras no se muevan antes de la lectura y las lecturas / escrituras no se mueven después de la escritura).


Soy un firme creyente en eso, si está usando interbloqueado para incrementar los datos compartidos, entonces debería usarlo entrelazado en cualquier lugar al que acceda a esos datos compartidos. Del mismo modo, si usa insertar su primitiva de sincronización favorita aquí para incrementar los datos compartidos, entonces debería usar insertar su primitiva de sincronización favorita aquí en todas partes donde acceda a los datos compartidos.

int localNumberOfUpdates = Interlocked.CompareExchange(ref numberOfUpdates, 0, 0);

Te dará exactamente lo que buscas. Como otros han dicho, las operaciones entrelazadas son atómicas. Por lo tanto, Interlocked.CompareExchange siempre devolverá el valor más reciente. Lo uso todo el tiempo para acceder a datos compartidos simples como contadores.

No estoy tan familiarizado con Thread.VolatileRead, pero sospecho que también devolverá el valor más reciente. Me quedo con los métodos entrelazados, aunque solo sea por ser consistente.

Información adicional:

Recomiendo echarle un vistazo a la respuesta de Jon Skeet para saber por qué querrá alejarse de Thread.VolatileRead (): Thread.VolatileRead Implementación

Eric Lippert habla sobre la volatilidad y las garantías ofrecidas por el modelo de memoria C # en su blog en http://blogs.msdn.com/b/ericlippert/archive/2011/06/16/atomicity-volatility-and-immutability-are-different-part-three.aspx . Desde la boca de los caballos: "No intento escribir ningún código de bloqueo bajo, excepto para los usos más triviales de las operaciones entrelazadas. Dejo el uso de" volátil "a expertos reales".

Y estoy de acuerdo con el punto de Hans de que el valor siempre estará obsoleto al menos en unos pocos ns, pero si tiene un caso de uso en el que eso es inaceptable, probablemente no sea adecuado para un lenguaje recolectado como C # o un no real. tiempo OS. Joe Duffy tiene un buen artículo sobre la puntualidad de los métodos entrelazados aquí: http://joeduffyblog.com/2008/06/13/volatile-reads-and-writes-and-timeliness/


Thread.VolatileRead(numberOfUpdates) es lo que quieres. numberOfUpdates es un Int32 , por lo que ya tiene la atomicidad de forma predeterminada, y Thread.VolatileRead se asegurará de que se Thread.VolatileRead volatilidad.

Si numberOfUpdates se define como volatile int numberOfUpdates; no tiene que hacer esto, ya que todas las lecturas ya serán lecturas volátiles.

Parece haber confusión sobre si Interlocked.CompareExchange es más apropiado. Considere los siguientes dos extractos de la documentación.

De la documentación de Thread.VolatileRead :

Lee el valor de un campo. El valor es el último escrito por cualquier procesador en una computadora, independientemente de la cantidad de procesadores o el estado de la memoria caché del procesador.

De la documentación de Interlocked.CompareExchange :

Compara dos enteros con signo de 32 bits para la igualdad y, si son iguales, reemplaza uno de los valores.

En términos del comportamiento declarado de estos métodos, Thread.VolatileRead es claramente más apropiado. No desea comparar numberOfUpdates con otro valor, y no desea reemplazar su valor. Quieres leer su valor.

Lasse hace un buen punto en su comentario: es posible que sea mejor usar un bloqueo simple. Cuando el otro código quiere actualizar numberOfUpdates , hace algo como lo siguiente.

lock (state) { state.numberOfUpdates++; }

Cuando quieres leerlo, haces algo como lo siguiente.

int value; lock (state) { value = state.numberOfUpdates; }

Esto asegurará sus requisitos de atomicidad y volatilidad sin profundizar en primitivas multihilo más oscuras y relativamente bajas.