safe - C#4.0: ¿Hay propiedades autoejecutables listas para usar y seguras para hilos?
thread safe list c# (4)
Me gustaría tener acceso de lectura y escritura seguro de subprocesos a una propiedad implementada automáticamente. Me falta esta funcionalidad desde el framework C # / .NET, incluso en su última versión. En el mejor de los casos, esperaría algo como
[Threadsafe]
public int? MyProperty { get; set; }
Soy consciente de que hay varios ejemplos de código para lograr esto, pero solo quería estar seguro de que esto aún no es posible usando solo métodos de framework .NET, antes de implementar algo yo mismo. ¿Me equivoco?
EDITAR: Como algunas respuestas elaboran sobre atomicidad, quiero decir que solo quiero tener eso, por lo que yo entiendo: siempre y cuando (y no más de) un hilo esté leyendo el valor de la propiedad, no hay otro hilo está permitido cambiar el valor. Entonces, multi-threading no introduciría valores inválidos. Elegí el int? escriba porque ese es el que estoy preocupado actualmente.
EDIT2: He encontrado la respuesta específica al ejemplo con Nullable aquí, por Eric Lippert
Correcto; no hay tal dispositivo. Presumiblemente, está tratando de protegerse contra la lectura del campo, mientras que otro hilo ha cambiado la mitad (atomicidad)? Tenga en cuenta que muchas primitivas (pequeñas) son intrínsecamente seguras para este tipo de problemas de enhebrado:
5.5 Atomicidad de las referencias variables
Las lecturas y escrituras de los siguientes tipos de datos son atómicos:
bool
,char
,byte
,sbyte
,short
,ushort
,uint
,int
,float
y tipos de referencia . Además, las lecturas y escrituras de tipos enum con un tipo subyacente en la lista anterior también son atómicas.
Pero honestamente, esto es solo la punta del iceberg que se enhebra; por sí mismo, generalmente no es suficiente tener una propiedad segura para subprocesos; la mayoría de las veces el alcance de un bloque sincronizado debe ser más que una sola lectura / escritura.
También hay muchas maneras diferentes de hacer algo seguro para subprocesos, según el perfil de acceso;
-
lock
? -
ReaderWriterLockSlim
? - cambio de referencia a alguna clase (esencialmente un
Box<T>
, por lo que unBox<int?>
en este caso) -
Interlocked
(en todas las formas) -
volatile
(en algunos escenarios, no es una varita mágica ...) - etc
(sin mencionar que es inmutable (ya sea a través del código o simplemente eligiendo no mutarlo , que a menudo es la forma más sencilla de hacerlo seguro para subprocesos)
Estoy respondiendo aquí para agregar a la respuesta de Marc, donde dice "también hay muchas maneras diferentes de hacer algo seguro para subprocesos, según el perfil de acceso".
Solo quiero agregar, esa es la razón de esto, es que hay tantas maneras de no ser seguro para subprocesos, que cuando decimos que algo es seguro para subprocesos, tenemos que tener claro qué tipo de seguridad se proporciona.
Con casi cualquier objeto mutable, habrá formas de solucionarlo que no sean seguras para subprocesos (tenga en cuenta que casi cualquier excepción se acerca). Considere una cola segura para subprocesos que tenga los siguientes miembros (seguros para subprocesos); una operación en cola, una operación de dequeue y una propiedad de conteo. Es relativamente fácil construir uno de estos ya sea mediante el bloqueo interno de cada miembro, o incluso con técnicas de bloqueo.
Sin embargo, digamos que utilizamos el objeto de esta manera:
if(queue.Count != 0)
return queue.Dequeue();
El código anterior no es seguro para subprocesos, porque no hay garantía de que después de que el Count
(thread-safe) devuelva 1, otro subproceso no se dequeará y, por lo tanto, hará que la segunda operación falle.
Sigue siendo un objeto seguro para subprocesos de muchas maneras, especialmente porque incluso en este caso de error, la operación de dequeue anómala no colocará el objeto en un estado no válido.
Para hacer que un objeto sea seguro para todos los hilos frente a cualquier combinación dada de operaciones, tenemos que hacer que sea lógicamente inmutable (es posible tener una mutabilidad interna con operaciones de seguridad de subprocesos actualizando el estado interno como una optimización, por ejemplo, a través de la memorización o cargar desde un origen de datos según sea necesario, pero desde el exterior debe parecer inmutable) o reducir severamente el número de operaciones externas posibles (podríamos crear una cola segura para subprocesos que solo tenía Enqueue
y TryDequeue
que siempre es seguro para subprocesos pero que tanto reduce las operaciones posibles, y también fuerza a que una dequeue fallida sea redefinida como no un fallo, y fuerza un cambio en la lógica del código de llamada de la versión que teníamos antes).
Cualquier otra cosa es una garantía parcial. Obtenemos algunas garantías parciales gratis (como señala Marc, actuar sobre algunas propiedades automáticas ya son seguras para subprocesos con respecto a ser individualmente atómicas, lo que en algunos casos es toda la seguridad de subprocesos que necesitamos, pero en otros casos no va a ninguna parte). lo suficientemente cerca).
Consideremos un atributo que agrega esta garantía parcial a aquellos casos en los que aún no lo tenemos. ¿Cuánto valor tiene para nosotros? Bueno, en algunos casos será perfecto, pero en otros no. Volviendo a nuestro caso de prueba antes de la retirada, tener esa garantía en Count
no es de mucha utilidad, teníamos esa garantía y el código aún fallaba en condiciones de subprocesos múltiples de una manera que no lo haría en condiciones de subproceso único.
Además, agregar esta garantía a los casos que aún no la requieren requiere al menos un cierto nivel de sobrecarga. Puede ser una optimización prematura preocuparse por la sobrecarga todo el tiempo, pero agregar sobrecarga sin ganancia es una pesimismo prematura, ¡así que no hagamos eso! Además, si proporcionamos el control de concurrencia más amplio para hacer que un conjunto de operaciones sea realmente seguro para las hebras, entonces habremos hecho que los controles de concurrencia más pequeños sean irrelevantes y se conviertan en una sobrecarga pura, por lo que ni siquiera sacamos valor de nuestra sobrecarga en algunos casos; casi siempre es puramente basura.
Tampoco está claro qué tan amplios o estrechos son los problemas de concurrencia. ¿Necesitamos bloquear (o similar) solo en esa propiedad, o debemos bloquear todas las propiedades? ¿Necesitamos bloquear también operaciones no automáticas, y eso es posible?
Aquí no hay una buena respuesta (pueden ser preguntas difíciles de contestar al generar su propia solución, no importa al intentar responderla en el código que produciría dicho código cuando alguien más haya usado este atributo [Threadsafe]).
Además, cualquier enfoque dado tendrá un conjunto diferente de condiciones en las que pueden producirse interbloqueos, interbloqueos y problemas similares, por lo que podemos reducir la seguridad de subprocesos al tratar la seguridad de subprocesos como algo que podemos aplicar ciegamente a una propiedad.
Sin poder encontrar una única respuesta universal a esas preguntas, no hay una buena manera de proporcionar una única implementación universal, y cualquier atributo [Threadsafe] tendría un valor muy limitado en el mejor de los casos. Finalmente, a nivel psicológico del programador que lo usa, es muy probable que lleve a una falsa sensación de seguridad de que han creado una clase segura para subprocesos cuando en realidad no lo han hecho; que lo haría realmente peor que inútil.
No, no es posible No hay almuerzo gratis aquí. En el momento en que sus propiedades automáticas necesiten incluso una propina más (seguridad de subprocesos, INotifyPropertyChanged), es tarea suya: no tiene propiedades automáticas mágicas.
Según la especificación de C # 4.0, este comportamiento no se modifica:
Sección 10.7.3 Propiedades implementadas automáticamente
Cuando una propiedad se especifica como una propiedad implementada automáticamente, un campo oculto de respaldo está automáticamente disponible para la propiedad, y los accesores se implementan para leer y escribir en ese campo de respaldo.
El siguiente ejemplo:
public class Point {
public int X { get; set; } // automatically implemented
public int Y { get; set; } // automatically implemented
}
es equivalente a la siguiente declaración:
public class Point {
private int x;
private int y;
public int X { get { return x; } set { x = value; } }
public int Y { get { return y; } set { y = value; } }
}