¿Debo bloquear o marcar como volátil cuando accedo a un indicador booleano simple en C #?

Solo digamos que tiene una operación simple que se ejecuta en un hilo de fondo. Desea proporcionar una forma de cancelar esta operación para que cree un indicador booleano que establezca como verdadero desde el controlador de evento click de un botón cancelar.

private bool _cancelled; private void CancelButton_Click(Object sender ClickEventArgs e) { _cancelled = true; } 

Ahora está configurando el indicador de cancelación desde el hilo de la GUI, pero lo está leyendo desde el hilo de fondo. ¿Necesita bloquear antes de acceder al bool?

¿Debería hacer esto (y obviamente también bloquear el controlador de evento click click)?

 while(operationNotComplete) { // Do complex operation lock(_lockObject) { if(_cancelled) { break; } } } 

O es aceptable hacer esto (sin locking):

 while(!_cancelled & operationNotComplete) { // Do complex operation } 

O qué tal marcar la variable cancelada como volátil. ¿Es eso necesario?

[Sé que existe la clase BackgroundWorker con su método incorporado Incorporated CancelAsync (), pero estoy interesado en la semántica y el uso del acceso variable con locking y enhebrado aquí, no en la implementación específica, el código es solo un ejemplo.]

Parece que hay dos teorías.

1) Debido a que es un tipo simple incorporado (y el acceso a tipos incorporados es atómico en .net) y porque solo estamos escribiendo en un solo lugar y solo leyendo en el hilo de fondo no hay necesidad de bloquearlo o marcarlo como volátil.
2) Debe marcarlo como volátil porque si no lo hace, el comstackdor puede optimizar la lectura en el ciclo while porque no cree que sea capaz de modificar el valor.

¿Cuál es la técnica correcta? (¿Y por qué?)

[Editar: Parece que hay dos escuelas de pensamiento claramente definidas y opuestas sobre esto. Estoy buscando una respuesta definitiva sobre esto, así que, si es posible, publique sus razones y cite sus fonts junto con su respuesta.]

En primer lugar, el enhebrado es complicado ;-p

Sí, a pesar de todos los rumores que indican lo contrario, se requiere usar lock o volatile (pero no ambos) al acceder a bool desde múltiples hilos.

Para tipos simples y acceso como un indicador de salida ( bool ), entonces volatile es suficiente: esto asegura que los hilos no almacenan en caché el valor en sus registros (es decir: uno de los hilos nunca ve actualizaciones).

Para valores más grandes (donde la atomicidad es un problema), o donde desea sincronizar una secuencia de operaciones (un ejemplo típico es “si no existe y agrega” acceso al diccionario), un lock es más versátil. Esto actúa como una barrera de memoria, por lo que aún le da la seguridad del hilo, pero proporciona otras funciones como pulso / espera. Tenga en cuenta que no debe usar un lock en un valor-tipo o una string ; ni Type o this ; la mejor opción es tener su propio objeto de locking como un campo ( readonly object syncLock = new object(); ) y bloquearlo.

Para un ejemplo de lo mal que se rompe (es decir, bucle siempre) si no se sincroniza – ver aquí .

Para abarcar múltiples progtwigs, una primitiva del sistema operativo como un Mutex o *ResetEvent también puede ser útil, pero esto es excesivo para un solo exe.

_cancelled debe ser volatile . (si no elige bloquear)

Si un subproceso cambia el valor de _cancelled , es posible que otros subprocesos no vean el resultado actualizado.

Además, creo que las operaciones de lectura / escritura de _cancelled son atómicas :

La sección 12.6.6 de la especificación de la CLI establece: “Una CLI conforme debe garantizar que el acceso de lectura y escritura a ubicaciones de memoria alineadas correctamente no mayores que el tamaño de la palabra nativa es atómico cuando todos los accesos de escritura a una ubicación son del mismo tamaño”.

No es necesario bloquear porque tiene un único escenario de escritor y un campo booleano es una estructura simple sin riesgo de corromper el estado ( mientras que era posible obtener un valor booleano que no es falso ni verdadero ). Pero debe marcar el campo como volatile para evitar que el comstackdor realice algunas optimizaciones. Sin el modificador volatile el comstackdor podría almacenar en caché el valor de un registro durante la ejecución de su bucle en su hilo de trabajo y, a su vez, el bucle nunca reconocería el valor modificado. Este artículo de MSDN ( Cómo: Crear y finalizar subprocesos (Guía de progtwigción C #) ) resuelve este problema. Si bien es necesario bloquear, un locking tendrá el mismo efecto que marcar el campo volatile .

Para la sincronización de subprocesos, se recomienda que utilice una de las clases EventWaitHandle , como ManualResetEvent . Si bien es marginalmente más simple emplear un indicador booleano simple como lo hace aquí (y sí, querría marcarlo como volatile ), IMO es mejor entrar en la práctica de usar las herramientas de subprocesamiento. Para tus propósitos, harías algo como esto …

 private System.Threading.ManualResetEvent threadStop; void StartThread() { // do your setup // instantiate it unset threadStop = new System.Threading.ManualResetEvent(false); // start the thread } 

En tu hilo …

 while(!threadStop.WaitOne(0) && !operationComplete) { // work } 

Luego, en la GUI para cancelar …

 threadStop.Set(); 

Busque Interlocked.Exchange () . Hace una copia muy rápida en una variable local que se puede usar para comparar. Es más rápido que lock ().