C# ThreadStatic + miembros volátiles que no funcionan como se esperaba

Estaba leyendo el consejos y trucos post y pensé en probar algunas de las cosas de C# que nunca había hecho antes. Por lo tanto, el siguiente código no tiene ningún propósito real, sino que es solo una 'función de prueba' para ver qué sucede.

De todos modos, tengo dos campos privados estáticos:

private static volatile string staticVolatileTestString = "";
[ThreadStatic]
private static int threadInt = 0;

Como puedes ver, estoy probando ThreadStaticAttribute y volátiles palabra clave.

De todos modos, tengo un método de prueba que se ve así:

private static string TestThreadStatic() {
    // Firstly I'm creating 10 threads (DEFAULT_TEST_SIZE is 10) and starting them all with an anonymous method
    List<Thread> startedThreads = new List<Thread>();
    for (int i = 0; i < DEFAULT_TEST_SIZE; ++i) {
        Thread t = new Thread(delegate(object o) {
            // The anon method sets a newValue for threadInt and prints the new value to the volatile test string, then waits between 1 and 10 seconds, then prints the value for threadInt to the volatile test string again to confirm that no other thread has changed it
            int newVal = randomNumberGenerator.Next(10, 100);
            staticVolatileTestString += Environment.NewLine + "\tthread " + ((int) o) + " setting threadInt to " + newVal;
            threadInt = newVal;
            Thread.Sleep(randomNumberGenerator.Next(1000, 10000));
            staticVolatileTestString += Environment.NewLine + "\tthread " + ((int) o) + " finished: " + threadInt;
        });
        t.Start(i);
        startedThreads.Add(t);
    }

    foreach (Thread th in startedThreads) th.Join();

    return staticVolatileTestString;
}

Lo que esperaría ver devuelto por esta función es un resultado como este:

thread 0 setting threadInt to 88
thread 1 setting threadInt to 97
thread 2 setting threadInt to 11
thread 3 setting threadInt to 84
thread 4 setting threadInt to 67
thread 5 setting threadInt to 46
thread 6 setting threadInt to 94
thread 7 setting threadInt to 60
thread 8 setting threadInt to 11
thread 9 setting threadInt to 81
thread 5 finished: 46
thread 2 finished: 11
thread 4 finished: 67
thread 3 finished: 84
thread 9 finished: 81
thread 6 finished: 94
thread 7 finished: 60
thread 1 finished: 97
thread 8 finished: 11
thread 0 finished: 88

Sin embargo, lo que obtengo es esto:

thread 0 setting threadInt to 88
thread 4 setting threadInt to 67
thread 6 setting threadInt to 94
thread 7 setting threadInt to 60
thread 8 setting threadInt to 11
thread 9 setting threadInt to 81
thread 5 finished: 46
thread 2 finished: 11
thread 4 finished: 67
thread 3 finished: 84
thread 9 finished: 81
thread 6 finished: 94
thread 7 finished: 60
thread 1 finished: 97
thread 8 finished: 11
thread 0 finished: 88

La segunda 'mitad' de la salida es la esperada (lo que supongo que significa que el campo ThreadStatic funciona como pensaba), pero parece que algunas de las salidas iniciales se han 'omitido' de la primera 'mitad'.

Además, los subprocesos en la primera 'mitad' están desordenados, pero entiendo que un subproceso no se ejecuta inmediatamente tan pronto como llamas a Start(); pero en cambio, los controles internos del sistema operativo iniciarán los hilos como mejor les parezca. EDITAR: No, no lo son, en realidad, solo pensé que lo eran porque mi cerebro pierde los números consecutivos


Entonces, mi pregunta es: ¿Qué está mal para que pierda algunas líneas en la primera 'mitad' de la salida? Por ejemplo, ¿dónde está la línea 'subproceso 3 ajuste threadInt a 84'?

preguntado el 12 de junio de 12 a las 10:06

Ejecuto su código varias veces y siempre obtengo el resultado esperado... -

Apostaría a que en algún momento, la llamada a += en la cadena obtiene el valor antes de que el subproceso anterior lo haya establecido y el nuevo subproceso lo sobrescribe con el nuevo valor (por lo tanto, se omitió la salida del subproceso 1). -

@DannyChen Estoy en un quad-core, y ese no es el único código que se ejecuta en la aplicación; No sé si alguno de los dos tendría sentido. Puedo cargar el código completo en algún lugar (son solo dos archivos .cs) si lo desea. -

@Charleh, bueno, eso es lo que pensé, y dudo que sea un error en el marco ... -

Esto no tiene que ver nada con el quad-core. Lo mismo puede suceder en un solo núcleo también. Depende de cuándo exactamente el planificador decide pausar/reanudar los hilos. Eso puede Sucede más a menudo en un quad-core, pero también podría ser al revés... -

2 Respuestas

Los subprocesos se ejecutan al mismo tiempo. Lo que ocurre conceptualmente es esto:

staticVolatileTestString += Environment.NewLine + "\tthread " + ((int) o) + " setting threadInt to " + newVal;
  1. El subproceso 1 lee staticVolatileTestString
  2. El subproceso 2 lee staticVolatileTestString
  3. El subproceso 3 lee staticVolatileTestString
  4. El subproceso 1 agrega las cosas y escribe staticVolatileTestString de vuelta
  5. El subproceso 2 agrega las cosas y escribe staticVolatileTestString de vuelta
  6. El subproceso 3 agrega las cosas y escribe staticVolatileTestString de vuelta

Eso hace que tus líneas se pierdan. Volátil no ayuda aquí; todo el proceso de concatenación de la cadena no es atómico. Necesitas usar un candado alrededor de esas operaciones:

private static object sync = new object();

lock (sync) {
    staticVolatileTestString += Environment.NewLine + "\tthread " + ((int) o) + " setting threadInt to " + newVal;
}

Respondido el 12 de junio de 12 a las 10:06

El MSDN describe lo que la palabra clave volatileaquí:

La palabra clave volatile indica que un campo puede ser modificado por varios subprocesos que se ejecutan simultáneamente. Los campos que se declaran volátiles no están sujetos a las optimizaciones del compilador que asumen el acceso de un solo subproceso. Esto garantiza que el valor más actualizado esté presente en el campo en todo momento.

Esto significa, en tu ejemplo, lo que sucede es sobre nosotros esto (esto puede variar de vez en cuando; depende del planificador):

  • el hilo 0 está leyendo la cadena de staticVolatileTestString
  • el subproceso 0 está agregando 'subproceso 0 configurando threadInt a 88'
  • el hilo 0 está escribiendo la cadena de nuevo en staticVolatileTestString

esto es como se esperaba hasta ahora, pero luego:

  • hilo 1-4 están leyendo la cadena de staticVolatileTestString
  • el subproceso 1 está agregando 'subproceso 1 configurando threadInt a 97'
  • el subproceso 2 está agregando 'subproceso 2 configurando threadInt a 11'
  • el hilo 2 está escribiendo la cadena de nuevo en staticVolatileTestString
  • ... hilos 1, 2, 3 están leyendo y agregando, y escribiendo
  • el hilo 4 está escribiendo la cadena de nuevo en staticVolatileTestString
  • y así...

ver lo que pasó aquí? el subproceso 4 leyó la cadena 'subproceso 0 configurando threadInt en 88', agregó su 'subproceso 4...' y lo volvió a escribir, sobrescribiendo todo lo que los subprocesos 1, 2 y 3 ya habían escrito en la cadena.

Respondido el 12 de junio de 12 a las 10:06

Entonces, ¿está diciendo que los subprocesos leen el valor simultáneamente y luego lo escriben como una operación separada uno sobre el otro? Si ese es el caso, necesitaría algún tipo de bloqueo en el campo (que es lo que pensé que era volátil, pero supongo que en realidad solo asegura que no haya almacenamiento en caché, ¿verdad?) EDITAR: la respuesta de Lucero confirma esto: Xenoprimato

@Motig: el bloqueo está allí cuando se lee la cadena o se vuelve a escribir, pero no en el medio. Recuerde: las cadenas son inmutables, por lo tanto, se crea una nueva instancia de cadena y la referencia a ella se vuelve a escribir de forma segura en la variable. - Felipe Daubmeier

@Motig: la palabra clave volátil funciona muy bien con tipos de datos primitivos mutables (bool, int, double, etc.). Sin embargo, con las cadenas, desea establecer un bloqueo dedicado, leer la cadena, modificarla y escribirla de nuevo, luego liberar el bloqueo. - Felipe Daubmeier

@Felipe: Ten cuidado. Las lecturas y escrituras atómicas no están garantizadas para todos esos tipos. Sección 12.5 de la Especificaciones de ECMA C # dice lo siguiente: "Lee y escribe de otros tipos, incluyendo long, ulong, doubley decimal, así como los tipos definidos por el usuario, no necesitan ser atómicos". Si desea atomicidad garantizada para esos tipos, también necesitará un candado, o tal vez uno de los Interlocked métodos. - LucasH

@Philip: Sí, o Interlocked.Add. - LucasH

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.