2009-07-12 13 views
6

Tengo un poco de mi juego que se parece a esto:C# Interlocked Cambio

public static float Time; 

float someValue = 123; 
Interlocked.Exchange(ref Time, someValue); 

quiero cambiar tiempo para ser un Uint32; sin embargo, cuando trato de usar UInt32 en lugar de float para los valores, se pone de manifiesto que el tipo debe ser de tipo de referencia. Float no es un tipo de referencia, por lo que sé que es técnicamente posible hacerlo con tipos que no sean de referencia. ¿Hay alguna forma práctica de hacer que esto funcione con UInt32?

Respuesta

12

Aunque feo, es realidad es posible realizar una atómica Cambio o CompareExchange en una enumeración o de otro tipo de valor blittable de 64 bits o menos usando unsafe C# código:

enum MyEnum { A, B, C }; 

MyEnum m_e = MyEnum.B; 

unsafe void example() 
{ 
    MyEnum e = m_e; 
    fixed (MyEnum* ps = &m_e) 
     if (Interlocked.CompareExchange(ref *(int*)ps, (int)(e | MyEnum.C), (int)e) == (int)e) 
     { 
      /// change accepted, m_e == B | C 
     } 
     else 
     { 
      /// change rejected 
     } 
} 

El La parte contraria a la intuición es que la expresión ref en el puntero desreferenciado hace que realmente penetra a través de la dirección de la enumeración. Creo que el compilador habría tenido derecho a generar una variable temporal invisible en la pila, en cuyo caso esto no funcionaría. Úselo bajo su propio riesgo.

[editar: para el tipo específico solicitado por la OP]

static unsafe uint CompareExchange(ref uint target, uint v, uint cmp) 
{ 
    fixed (uint* p = &target) 
     return (uint)Interlocked.CompareExchange(ref *(int*)p, (int)v, (int)cmp); 
} 

[edit: y 64 bits sin signo larga]

static unsafe ulong CompareExchange(ref ulong target, ulong v, ulong cmp) 
{ 
    fixed (ulong* p = &target) 
     return (ulong)Interlocked.CompareExchange(ref *(long*)p, (long)v, (long)cmp); 
} 

(también probé con el indocumentado C# palabra clave __makeref para lograr esto, pero esto no funciona porque no puede usar ref en un dreferenced __refvalue. Es una lástima, porque el CLR asigna las funciones InterlockedExchange a una función interna privada que opera s en TypedReference [Comentario debatida por intercepción JIT, ver más abajo])


[editar: April 2017] Recientemente he aprendido que cuando .NET se ejecuta en modo de 32 bits (o, es decir, en el WOW subsistema), las operaciones Interlocked de 64 bits son no garantizadas para ser atómicas con respecto a no-Interlocked, vistas "externas" de las mismas ubicaciones de memoria. En el modo de 32 bits, la garantía atómica solo se aplica globalmente en los accesos QWORD que utilizan las funciones Interlocked (y tal vez Volatile.*, o Thread.Volatile*, TBD?).

En otras palabras, para obtener operaciones atómicas de 64 bits en modo de 32 bits, todo accesos a QWord ubicaciones deben ocurrir a través de Interlocked con el fin de preservar las garantías, y no se puede conseguir si se asume que linda (p. ej.) las lecturas directas están protegidas simplemente porque usted siempre usa las funciones Interlocked para escribir.

Finalmente, tenga en cuenta que las funciones Interlocked en el CLR son especialmente reconocidas por el compilador .NET JIT y reciben un tratamiento especial. Ver here y here Este hecho puede ayudar a explicar la contra-intuición que mencioné anteriormente.

+0

De hecho, voy a dar la respuesta aceptada porque podría escribir un CompareExchange para UInt32 de esta manera, que fue la pregunta original. – Martin

+0

Es bueno saber que esto funciona, pero creo que su advertencia de "uso bajo su propio riesgo" debe enfatizarse muy fuertemente. Si la próxima versión del compilador generó una temperatura aquí, introduciría silenciosamente un error que podría ser muy difícil de rastrear. No soy un gran probador de unidades, pero este tipo de truco básicamente exige uno. Aún así, es bueno saberlo. Up-votado. – Gabriel

+0

No lo he probado, pero sospecho que esto es posible sin 'inseguro' usando DynamicMethod & IL-generation. (Dado que es posible para enumeraciones, [como se muestra aquí] (http://stackoverflow.com/a/18359360/533837). Sigue siendo un "truco", pero no creo que dependa de la implementación del compilador . – AnorZaken

2

Quizás use int en lugar de uint; hay sobrecargas para int. ¿Necesitas el poco más de rango? Si es así, envía/convierte lo más tarde posible.

+0

Bueno, yo nunca use valores inferiores a cero (ya que está almacenando vez desde que comenzó el juego), sin embargo (2^32)/2 sigue siendo un juego muy largo (que está almacenando milisegundos. Supongo que por ahora solo usaré los enteros y lo dejaré así. – Martin

16

Hay una sobrecarga de Interlocked.Exchange específicamente para float (y otros para double, int, long, IntPtr y object). No hay uno para uint, por lo que el compilador calcula que la coincidencia más cercana es la genérica Interlocked.Exchange<T> - pero en ese caso T tiene que ser un tipo de referencia. uint no es un tipo de referencia, por lo que tampoco funciona; de ahí el mensaje de error.

En otras palabras:

En cuanto a lo que debe hacer, las opciones son cualquiera de:

  • Potencialmente utilizar int lugar, como Marc sugiere.
  • Si necesita un rango extra, piense en usar long.
  • Uso uint pero no trate de escribir código libre de bloqueo

Aunque, obviamente, Exchange funciona bien con algunos tipos de valor específicas, Microsoft no ha puesto en práctica para todos los tipos primitivos. No me puedo imaginar que hubiera sido difícil hacerlo (son solo bits, después de todo) pero supuestamente querían mantener la cuenta atrás de la sobrecarga.

+0

'Interlocked' deberían proporcionar sobrecargas para cada blittable de 64 bits no trivial (o menor) Tipo de valor en el BCL y no hay excusa para cualquier cosa menos. Irónicamente, a diferencia nativo de C/C++ (donde el compilador de optimización fetiche debilita las garantías de acceso a memoria en detrimento general de diseño libre ** **-bloqueo), el modelo de memoria férrea en .NET permite a-o de hecho incluso anima-amplio uso del código de bloqueo de forma no trivial. Dada esta gran importancia de las operaciones atómicas, si [mi solución alternativa] (/ a/5589515/147511) no estuviera disponible, la supervisión sería francamente espantosa. –

-3

no se puede pasar una expresión fundido por referencia, se debe utilizar una variable temporal:

public static float Time; 
float value2 = (float)SomeValue; 
Interlocked.Exchange(ref Time, ref value2); 
SomeValue = value2; 
+0

Oh, el elenco fue sólo para demostrar qué tipo de valor que es, no me di cuenta que no puede utilizar moldes allí: O – Martin

+0

la segunda variable no tiene por qué ser una referencia, mira en el MSDN aquí: http : //msdn.microsoft.com/en-us/library/5z8f2s39.aspx – Martin

+5

Realización de una copia de la variable en contra del propósito de Interlocked.Exchange. –

2

Todavía es un truco, pero es posible hacer esto con la generación IL en lugar de usar el código unsafe. El beneficio es que en lugar de confiar en un detalle de implementación del compilador, se basa en el hecho de que los tipos con y sin firma tienen la misma longitud de bit, que es parte de la especificación.

Aquí es cómo:

using System; 
using System.Reflection; 
using System.Reflection.Emit; 
using ST = System.Threading; 

/// <summary> 
/// Provides interlocked methods for uint and ulong via IL-generation. 
/// </summary> 
public static class InterlockedUs 
{ 
    /// <summary> 
    /// Compares two 32-bit unsigned integers for equality and, if they are equal, 
    /// replaces one of the values. 
    /// </summary> 
    /// <param name="location"> 
    /// The value to exchange, i.e. the value that is compared with <paramref name="comparand"/> and 
    /// possibly replaced with <paramref name="value"/>.</param> 
    /// <param name="value"> 
    /// The value that replaces the <paramref name="location"/> value if the comparison 
    /// results in equality.</param> 
    /// <param name="comparand"> 
    /// A value to compare against the value at <paramref name="location"/>.</param> 
    /// <returns>The original value in <paramref name="location"/>.</returns> 
    public static uint CompareExchange(ref uint location, uint value, uint comparand) 
    { 
     return ceDelegate32(ref location, value, comparand); 
    } 

    /// <summary> 
    /// Compares two 64-bit unsigned integers for equality and, if they are equal, 
    /// replaces one of the values. 
    /// </summary> 
    /// <param name="location"> 
    /// The value to exchange, i.e. the value that is compared with <paramref name="comparand"/> and 
    /// possibly replaced with <paramref name="value"/>.</param> 
    /// <param name="value"> 
    /// The value that replaces the <paramref name="location"/> value if the comparison 
    /// results in equality.</param> 
    /// <param name="comparand"> 
    /// A value to compare against the value at <paramref name="location"/>.</param> 
    /// <returns>The original value in <paramref name="location"/>.</returns> 
    public static ulong CompareExchange(ref ulong location, ulong value, ulong comparand) 
    { 
     return ceDelegate64(ref location, value, comparand); 
    } 


    #region --- private --- 
    /// <summary> 
    /// The CompareExchange signature for uint. 
    /// </summary> 
    private delegate uint Delegate32(ref uint location, uint value, uint comparand); 

    /// <summary> 
    /// The CompareExchange signature for ulong. 
    /// </summary> 
    private delegate ulong Delegate64(ref ulong location, ulong value, ulong comparand); 

    /// <summary> 
    /// IL-generated CompareExchange method for uint. 
    /// </summary> 
    private static readonly Delegate32 ceDelegate32 = GenerateCEMethod32(); 

    /// <summary> 
    /// IL-generated CompareExchange method for ulong. 
    /// </summary> 
    private static readonly Delegate64 ceDelegate64 = GenerateCEMethod64(); 

    private static Delegate32 GenerateCEMethod32() 
    { 
     const string name = "CompareExchange"; 
     Type signedType = typeof(int), unsignedType = typeof(uint); 
     var dm = new DynamicMethod(name, unsignedType, new[] { unsignedType.MakeByRefType(), unsignedType, unsignedType }); 
     var ilGen = dm.GetILGenerator(); 
     ilGen.Emit(OpCodes.Ldarg_0); 
     ilGen.Emit(OpCodes.Ldarg_1); 
     ilGen.Emit(OpCodes.Ldarg_2); 
     ilGen.Emit(
      OpCodes.Call, 
      typeof(ST.Interlocked).GetMethod(name, BindingFlags.Public | BindingFlags.Static, 
       null, new[] { signedType.MakeByRefType(), signedType, signedType }, null)); 
     ilGen.Emit(OpCodes.Ret); 
     return (Delegate32)dm.CreateDelegate(typeof(Delegate32)); 
    } 

    private static Delegate64 GenerateCEMethod64() 
    { 
     const string name = "CompareExchange"; 
     Type signedType = typeof(long), unsignedType = typeof(ulong); 
     var dm = new DynamicMethod(name, unsignedType, new[] { unsignedType.MakeByRefType(), unsignedType, unsignedType }); 
     var ilGen = dm.GetILGenerator(); 
     ilGen.Emit(OpCodes.Ldarg_0); 
     ilGen.Emit(OpCodes.Ldarg_1); 
     ilGen.Emit(OpCodes.Ldarg_2); 
     ilGen.Emit(
      OpCodes.Call, 
      typeof(ST.Interlocked).GetMethod(name, BindingFlags.Public | BindingFlags.Static, 
       null, new[] { signedType.MakeByRefType(), signedType, signedType }, null)); 
     ilGen.Emit(OpCodes.Ret); 
     return (Delegate64)dm.CreateDelegate(typeof(Delegate64)); 
    } 
    #endregion 
} 

crédito a "hvd" a la idea de IL-generación y un código similar para un método CompareExchange para enumeraciones, que se puede encontrar here.

Habrá una sobrecarga para generar el método en la primera llamada, pero el método generado se almacena en forma de delegado, por lo que cualquier llamada posterior debería ser muy efectiva.

Y citar el enlace anterior:

La IL generado es verificable, al menos según PEVerify, como se puede comprobar al hacer este uso AssemblyBuilder y guardar el resultado en un archivo.

1

[editar:]Mea culpa y disculpas a @AnorZaken ya que mi respuesta es similar a la de él. Honestamente, no lo vi antes de publicar el mío. Lo guardaré por ahora en caso de que mi texto y mis explicaciones sean útiles o tengan ideas adicionales, pero el crédito por el trabajo previo corresponde a Anor.


Aunque tengo another solution en esta página, algunas personas podrían estar interesados ​​en un enfoque totalmente diferente. A continuación, me importa un DynamicMethod que implementa Interlocked.CompareExchange para cualquiera de 32 o 64 bits Tipo de blittable, que incluye cualquier costumbre Enum tipos, los tipos primitivos que el método integrado olvidó (uint, ulong), e incluso su propios ValueType casos - tanto tiempo como cualquiera de estos son DWORD (4-bytes, es decir, int, System.Int32) o QWord (8-bytes, long, System.Int64) de tamaño. Por ejemplo, el siguiente Enum tipo voluntad no trabajo ya que especifica un tamaño no predeterminado, byte:

enum ByteSizedEnum : byte { Foo }  // no: size is not 4 or 8 bytes 

Como con la mayoría DynamicMethod implementaciones de generada en tiempo de ejecución-IL, la C# código ISN Es hermoso de contemplar, pero para algunas personas el elegante código nativo IL y elegante JITted lo compensan. Por ejemplo, a diferencia del otro método que publiqué, este no usa el código unsafe C#.

Para permitir la inferencia automática del tipo genérico en el sitio de llamada, envolver el ayudante en una clase static:

public static class IL<T> where T : struct 
{ 
    // generic 'U' enables alternate casting for 'Interlocked' methods below 
    public delegate U _cmp_xchg<U>(ref U loc, U _new, U _old); 

    // we're mostly interested in the 'T' cast of it 
    public static readonly _cmp_xchg<T> CmpXchg; 

    static IL() 
    { 
     // size to be atomically swapped; must be 4 or 8. 
     int c = Marshal.SizeOf(typeof(T).IsEnum ? 
           Enum.GetUnderlyingType(typeof(T)) : 
           typeof(T)); 

     if (c != 4 && c != 8) 
      throw new InvalidOperationException("Must be 32 or 64 bits"); 

     var dm = new DynamicMethod(
      "__IL_CmpXchg<" + typeof(T).FullName + ">", 
      typeof(T), 
      new[] { typeof(T).MakeByRefType(), typeof(T), typeof(T) }, 
      MethodInfo.GetCurrentMethod().Module, 
      false); 

     var il = dm.GetILGenerator(); 
     il.Emit(OpCodes.Ldarg_0); // ref T loc 
     il.Emit(OpCodes.Ldarg_1); // T _new 
     il.Emit(OpCodes.Ldarg_2); // T _old 
     il.Emit(OpCodes.Call, c == 4 ? 
       ((_cmp_xchg<int>)Interlocked.CompareExchange).Method : 
       ((_cmp_xchg<long>)Interlocked.CompareExchange).Method); 
     il.Emit(OpCodes.Ret); 

     CmpXchg = (_cmp_xchg<T>)dm.CreateDelegate(typeof(_cmp_xchg<T>)); 
    } 
}; 

Técnicamente, lo anterior es todo lo que necesita. Ahora puede llamar al CmpXchgIL<T>.CmpXchg(...) en cualquier tipo de valor apropiado (como se explica en la introducción anterior), y se comportará exactamente como el Interlocked.CompareExchange(...) incorporado en System.Threading.Por ejemplo, digamos que usted tiene un struct que contienen dos enteros:

struct XY 
{ 
    public XY(int x, int y) => (this.x, this.y) = (x, y); // C#7 tuple syntax 
    int x, y; 
    static bool eq(XY a, XY b) => a.x == b.x && a.y == b.y; 
    public static bool operator ==(XY a, XY b) => eq(a, b); 
    public static bool operator !=(XY a, XY b) => !eq(a, b); 
} 

ahora puede publicar atómicamente la estructura de 64 bits tal como era de esperar con cualquier operacióncmpxchg. Esto publica atómicamente los dos enteros, de modo que es imposible que otro subproceso vea un emparejamiento "desgarrado" o incoherente. Huelga decir que hacerlo fácilmente con un emparejamiento lógico es muy útil en la programación concurrente, más aún si se diseña una estructura elaborada que empaqueta muchos campos en los 64 (o 32) bits disponibles. He aquí un ejemplo de la llamada in situ para hacer esto:

var xy = new XY(3, 4);  // initial value 

//... 

var _new = new XY(7, 8); // value to set 
var _exp = new XY(3, 4); // expected value 

if (IL<XY>.CmpXchg(ref xy, _new, _exp) != _exp) // atomically swap the 64-bit ValueType 
    throw new Exception("change not accepted"); 

encima, he mencionado que se puede poner en orden el sitio llamado al permitir la inferencia de tipos para que no tiene que especificar el parámetro genérico. Para ello, basta con definir un método estático genérica en una de sus clases globales no genéricos:

public static class my_globals 
{ 
    [DebuggerStepThrough, MethodImpl(MethodImplOptions.AggressiveInlining)] 
    public static T CmpXchg<T>(ref T loc, T _new, T _old) where T : struct => 
               _IL<T>.CmpXchg(ref loc, _new, _old); 
} 

te muestro el sitio llamado simplificada con un ejemplo diferente, esta vez utilizando una Enum:

using static my_globals; 

public enum TestEnum { A, B, C }; 

static void CompareExchangeEnum() 
{ 
    var e = TestEnum.A; 

    if (CmpXchg(ref e, TestEnum.B, TestEnum.A) != TestEnum.A) 
     throw new Exception("change not accepted"); 
} 

en cuanto a la pregunta original, ulong y uint trabajo trivialmente así:

ulong ul = 888UL; 

if (CmpXchg(ref ul, 999UL, 888UL) != 888UL) 
    throw new Exception("change not accepted"); 
Cuestiones relacionadas