c# c++ il interlocked interlocked-increment

c# - Interlocked.CompareExchange<Int> usando GreaterThan o LessThan en lugar de igualdad



c++ il (7)

El objeto System.Threading.Interlocked permite la suma (resta) y la comparación como una operación atómica. Parece que un CompareExchange que simplemente no hace igualdad, pero también GreaterThan / LessThan como una comparación atómica sería bastante valioso.

¿Un hipotético Interlocked.GreaterThan ¿ Interlocked.GreaterThan una característica de la IL o es una característica de nivel de CPU? ¿Ambos?

A falta de otra opción, ¿es posible crear una función de este tipo en C ++ o el código IL directo y exponer esa funcionalidad a C #?


¿Qué te parece esta implementación?

// this is a Interlocked.ExchangeIfGreaterThan implementation private static void ExchangeIfGreaterThan(ref long location, long value) { // read long current = Interlocked.Read(ref location); // compare while (current < value) { // set var previous = Interlocked.CompareExchange(ref location, value, current); // if another thread has set a greater value, we can break // or if previous value is current value, then no other thread has it changed in between if (previous == current || previous >= value) // note: most commmon case first break; // for all other cases, we need another run (read value, compare, set) current = Interlocked.Read(ref location); } }


Actualice a la última publicación que hice aquí: encontramos una mejor manera de realizar una comparación mayor mediante el uso de un objeto de bloqueo adicional. Escribimos muchas pruebas unitarias para validar que un bloqueo y un Enclavamiento pueden usarse juntos, pero solo en algunos casos.

Cómo funciona el código: entrelazado usa barreras de memoria que una lectura o escritura es atómica. El bloqueo de sincronización es necesario para hacer que una comparación mayor que una operación atómica. Entonces, la regla ahora es que dentro de esta clase ninguna otra operación escribe el valor sin este bloqueo de sincronización.

Lo que obtenemos con esta clase es un valor entrelazado que puede leerse muy rápido, pero la escritura toma un poco más. Leer es aproximadamente 2-4 veces más rápido en nuestra aplicación.

Aquí el código como vista:

Consulte aquí: http://files.thekieners.com/blogcontent/2012/ExchangeIfGreaterThan2.png

Aquí como código para copiar y pegar:

public sealed class InterlockedValue { private long _myValue; private readonly object _syncObj = new object(); public long ReadValue() { // reading of value (99.9% case in app) will not use lock-object, // since this is too much overhead in our highly multithreaded app. return Interlocked.Read(ref _myValue); } public bool SetValueIfGreaterThan(long value) { // sync Exchange access to _myValue, since a secure greater-than comparisons is needed lock (_syncObj) { // greather than condition if (value > Interlocked.Read(ref _myValue)) { // now we can set value savely to _myValue. Interlocked.Exchange(ref _myValue, value); return true; } return false; } } }


Con estos métodos de ayuda, no solo puede intercambiar valor, sino también detectar si fue reemplazado o no.

El uso se ve así:

int currentMin = 10; // can be changed from other thread at any moment int potentialNewMin = 8; if (InterlockedExtension.AssignIfNewValueSmaller(ref currentMin, potentialNewMin)) { Console.WriteLine("New minimum: " + potentialNewMin); }

Y aquí están los métodos:

public static class InterlockedExtension { public static bool AssignIfNewValueSmaller(ref int target, int newValue) { int snapshot; bool stillLess; do { snapshot = target; stillLess = newValue < snapshot; } while (stillLess && Interlocked.CompareExchange(ref target, newValue, snapshot) != snapshot); return stillLess; } public static bool AssignIfNewValueBigger(ref int target, int newValue) { int snapshot; bool stillMore; do { snapshot = target; stillMore = newValue > snapshot; } while (stillMore && Interlocked.CompareExchange(ref target, newValue, snapshot) != snapshot); return stillMore; } }


Esto no es realmente cierto, pero es útil pensar que la concurrencia viene en 2 formas:

  1. Bloqueo de concurrencia libre
  2. Bloqueo de concurrencia basada

No es cierto porque la concurrencia basada en el bloqueo del software termina implementándose mediante el uso de instrucciones atómicas libres de bloqueo en algún lugar de la pila (a menudo en el núcleo). Sin embargo, las instrucciones atómicas libres de bloqueo terminan adquiriendo un bloqueo de hardware en el bus de memoria. Entonces, en realidad, la concurrencia de bloqueo libre y la concurrencia basada en bloqueo son lo mismo.

Pero conceptualmente, a nivel de una aplicación de usuario, son dos formas distintas de hacer las cosas.

La concurrencia basada en el bloqueo se basa en la idea de "bloquear" el acceso a una sección crítica del código. Cuando un hilo ha "bloqueado" una sección crítica, ningún otro hilo puede tener código ejecutándose dentro de esa misma sección crítica. Esto generalmente se hace mediante el uso de "mutexes", que se interconectan con el planificador del sistema operativo y hacen que los subprocesos no puedan ejecutarse mientras esperan para ingresar a una sección crítica bloqueada. El otro enfoque es utilizar "bloqueos de giro" que hacen que un hilo gire en un bucle, sin hacer nada útil, hasta que la sección crítica esté disponible.

La concurrencia libre de bloqueos se basa en la idea de utilizar instrucciones atómicas (especialmente soportadas por la CPU), que el hardware garantiza que se ejecutan atómicamente. Interlocked.Increment es un buen ejemplo de concurrencia sin bloqueo. Simplemente llama instrucciones especiales de la CPU que hacen un incremento atómico.

Bloquear la concurrencia libre es difícil. Se vuelve particularmente difícil a medida que aumenta la longitud y la complejidad de las secciones críticas. Cualquier paso en una sección crítica puede ser ejecutado simultáneamente por cualquier cantidad de hilos a la vez, y pueden moverse a velocidades muy diferentes. Debe asegurarse de que, a pesar de eso, los resultados de un sistema en su conjunto siguen siendo correctos. Para algo como un incremento, puede ser simple (el cs es solo una instrucción). Para secciones críticas más complejas, las cosas pueden volverse muy complejas muy rápidamente.

La concurrencia basada en bloqueo también es difícil, pero no tan difícil como la concurrencia sin bloqueo. Le permite crear regiones de código arbitrariamente complejas y saber que solo 1 hilo lo está ejecutando en cualquier momento.

Sin embargo, la libre concurrencia de bloqueo tiene una gran ventaja: la velocidad. Cuando se usa correctamente, puede ser órdenes de magnitud más rápido que la concurrencia basada en bloqueo. Los bucles de giro son malos para las secciones críticas de larga ejecución porque desperdician recursos de CPU sin hacer nada. Los Mutexes pueden ser malos para las secciones críticas pequeñas porque introducen muchos gastos generales. Implican un cambio de modo como mínimo, y múltiples cambios de contexto en el peor de los casos.

Considere implementar el montón administrado. Llamar al sistema operativo cada vez que se llama "nuevo" sería horrible. Destruiría el rendimiento de tu aplicación. Sin embargo, al usar la concurrencia sin bloqueo es posible implementar asignaciones de memoria gen 0 utilizando un incremento interbloqueado (no sé si eso es lo que hace el CLR, pero me sorprendería si no lo fuera. Eso puede ser una GRAN acción). ahorros.

Hay otros usos, como en estructuras de datos libres de bloqueo, como pilas persistentes y árboles de avl. Usualmente usan "cas" (comparar e intercambiar).

Sin embargo, la razón por la que la concurrencia basada bloqueada y la concurrencia libre de bloqueo son realmente equivalentes se debe a los detalles de implementación de cada uno.

Los bloqueos de giro generalmente usan instrucciones atómicas (normalmente cas) en su condición de bucle. Mutexes necesita utilizar bloqueos de giro o actualizaciones atómicas de las estructuras internas del kernel en su implementación.

Las instrucciones atómicas se implementan a su vez utilizando bloqueos de hardware.

En cualquier caso, ambos tienen sus combinaciones de compromisos, generalmente centrados en la complejidad de los resultados. Mutexes puede ser más rápido y más lento que el código de bloqueo libre. El código libre de bloqueo puede ser más complejo y menos complejo que un mutex. El mecanismo apropiado a utilizar depende de circunstancias específicas.

Ahora para responder a tu pregunta:

Un método que realizó un intercambio de comparación entrelazado si fuera menor que lo que implicaría para las personas que llaman que no está utilizando bloqueos. No se puede implementar con una sola instrucción de la misma manera que se puede hacer un intercambio de incremento o comparación. Podría simularlo haciendo una resta (para calcular menos), con un intercambio de comparación entrelazado en un bucle. También puede hacerlo con un mutex (pero eso implicaría un bloqueo y, por lo tanto, usar "enclavado" en el nombre sería engañoso). ¿Es apropiado construir la versión "simulada entrelazada vía cas"? Eso depende. Si el código se llama con mucha frecuencia y tiene muy poca contención de hilos, la respuesta es sí. De lo contrario, puede convertir una operación O (1) con factores constantes moderadamente altos en un bucle infinito (o muy largo), en cuyo caso sería mejor usar un mutex.

La mayoría de las veces no vale la pena.


Mayor / Menor que e igual a ya son operaciones atómicas. Eso no aborda el comportamiento concurrente seguro de su aplicación aunque.

No tiene sentido hacerlos parte de la familia de Interlocked, entonces la pregunta es: ¿qué estás tratando de lograr realmente?


Puede construir otras operaciones atómicas a partir de InterlockedCompareExchange .

public static bool InterlockedExchangeIfGreaterThan(ref int location, int comparison, int newValue) { int initialValue; do { initialValue = location; if (initialValue >= comparison) return false; } while (System.Threading.Interlocked.CompareExchange(ref location, newValue, initialValue) != initialValue); return true; }


Todas las operaciones entrelazadas tienen soporte directo en el hardware.

Las operaciones entrelazadas y el tipo de datos atómicos son cosas diferentes. El tipo atómico es una función de nivel de biblioteca. En algunas plataformas y para algunos tipos de datos, se implementan métodos atómicos utilizando instrucciones entrelazadas. En este caso son muy efectivos.

En otros casos, cuando la plataforma no tiene operaciones entrelazadas o no están disponibles para algún tipo de datos en particular, la biblioteca implementa estas operaciones utilizando la sincronización apropiada (crit_sect, mutex, etc.).

No estoy seguro si Interlocked.GreaterThan es realmente necesario. De lo contrario, ya podría estar implementado. Si sabe bien dónde puede ser útil, estoy seguro de que todos aquí estarán encantados de escuchar esto.