Seguridad de hilo C#con get/set
locking properties (9)
Esta es una pregunta detallada para C #.
Supongamos que tengo una clase con un objeto y ese objeto está protegido por un bloqueo:
Object mLock = new Object();
MyObject property;
public MyObject MyProperty {
get {
return property;
}
set {
property = value;
}
}
Quiero que un hilo de sondeo pueda consultar esa propiedad. También quiero que el subproceso actualice propiedades de ese objeto ocasionalmente, y algunas veces el usuario puede actualizar esa propiedad, y el usuario desea poder ver esa propiedad.
¿El siguiente código bloqueará correctamente los datos?
Object mLock = new Object();
MyObject property;
public MyObject MyProperty {
get {
lock (mLock){
return property;
}
}
set {
lock (mLock){
property = value;
}
}
}
Por ''correctamente'', lo que quiero decir es, si quiero llamar
MyProperty.Field1 = 2;
o lo que sea, ¿se bloqueará el campo mientras realizo la actualización? Es la configuración realizada por el operador igual dentro del alcance de la función ''get'', o la función ''get'' (y por lo tanto el bloqueo) terminará primero, y luego se llamará a la configuración, y luego ''set'', evitando así ¿La cerradura?
Editar: Dado que aparentemente esto no funcionará, ¿qué lo hará? ¿Tengo que hacer algo como esto?
Object mLock = new Object();
MyObject property;
public MyObject MyProperty {
get {
MyObject tmp = null;
lock (mLock){
tmp = property.Clone();
}
return tmp;
}
set {
lock (mLock){
property = value;
}
}
}
que más o menos simplemente se asegura de que solo tengo acceso a una copia, lo que significa que si tuviera dos hilos llamados ''get'' al mismo tiempo, cada uno comenzaría con el mismo valor de Field1 (¿no?). ¿Hay alguna forma de hacer bloqueo de lectura y escritura en una propiedad que tenga sentido? ¿O debería limitarme a bloquear secciones de funciones en lugar de los datos en sí?
Solo para que este ejemplo tenga sentido: MyObject es un controlador de dispositivo que devuelve el estado de forma asíncrona. Le envío comandos a través de un puerto serie, y luego el dispositivo responde a esos comandos en su propio momento. En este momento, tengo un hilo que lo sondea por su estado ("¿Sigues ahí? ¿Puedes aceptar comandos?"), Un hilo que espera respuestas en el puerto serial ("Acabo de recibir la cadena de estado 2, todo está bien" ), y luego el subproceso UI que toma otros comandos ("El usuario quiere que hagas esto") y publica las respuestas del controlador ("Acabo de hacer el trabajo, ahora actualizo la interfaz de usuario con eso"). Es por eso que quiero asegurar el objeto en sí mismo, en lugar de los campos del objeto; eso sería una gran cantidad de bloqueos, a, y b, no todos los dispositivos de esta clase tienen el mismo comportamiento, solo el comportamiento general, así que tendría que codificar muchos diálogos individuales si individualizara los bloqueos.
¿Los bloqueos C # no sufren los mismos problemas de bloqueo que otros idiomas?
P.EJ
var someObj = -1;
// Thread 1
if (someObj = -1)
lock(someObj)
someObj = 42;
// Thread 2
if (someObj = -1)
lock(someObj)
someObj = 24;
Esto podría tener el problema de que ambos subprocesos finalmente obtengan sus bloqueos y cambien el valor. Esto podría conducir a algunos errores extraños. Sin embargo, no desea bloquear innecesariamente el objeto a menos que lo necesite. En este caso, debe considerar el doble bloqueo verificado.
// Threads 1 & 2
if (someObj = -1)
lock(someObj)
if(someObj = -1)
someObj = {newValue};
Sólo algo para tener en cuenta.
Como han señalado otros, una vez que devuelves el objeto del captador, pierdes el control sobre quién accede al objeto y cuándo. Para hacer lo que quieres hacer, tendrás que poner un candado dentro del objeto en sí.
Tal vez no entiendo la imagen completa, pero de acuerdo con su descripción, no parece que necesariamente deba tener un candado para cada campo individual. Si tiene un conjunto de campos que simplemente se leen y escriben a través de getters y setters, probablemente pueda salirse con la suya con un solo bloqueo para estos campos. Obviamente, es posible que serialice innecesariamente el funcionamiento de sus hilos de esta manera. Pero de nuevo, según su descripción, tampoco parece que esté accediendo agresivamente al objeto.
También sugiero usar un evento en lugar de usar un hilo para sondear el estado del dispositivo. Con el mecanismo de sondeo, tocará el candado cada vez que el hilo lo consulte. Con el mecanismo de evento, una vez que el estado cambia, el objeto notificaría a cualquier oyente. En ese momento, su hilo de "sondeo" (que ya no sería un sondeo) se activaría y obtendría el nuevo estado. Esto será mucho más eficiente.
Como ejemplo...
public class Status
{
private int _code;
private DateTime _lastUpdate;
private object _sync = new object(); // single lock for both fields
public int Code
{
get { lock (_sync) { return _code; } }
set
{
lock (_sync) {
_code = value;
}
// Notify listeners
EventHandler handler = Changed;
if (handler != null) {
handler(this, null);
}
}
}
public DateTime LastUpdate
{
get { lock (_sync) { return _lastUpdate; } }
set { lock (_sync) { _lastUpdate = value; } }
}
public event EventHandler Changed;
}
Su hilo de ''votación'' se vería más o menos así.
Status status = new Status();
ManualResetEvent changedEvent = new ManualResetEvent(false);
Thread thread = new Thread(
delegate() {
status.Changed += delegate { changedEvent.Set(); };
while (true) {
changedEvent.WaitOne(Timeout.Infinite);
int code = status.Code;
DateTime lastUpdate = status.LastUpdate;
changedEvent.Reset();
}
}
);
thread.Start();
El alcance del bloqueo en su ejemplo está en el lugar incorrecto; necesita estar en el alcance de la propiedad de la clase ''MyObject'' en lugar de su contenedor.
Si MyObject my object class se usa simplemente para contener los datos que un hilo desea escribir y otro (el hilo de la interfaz de usuario) para leer a partir de entonces, es posible que no necesite un setter y lo construya una vez.
También considere si ubicar bloqueos en el nivel de propiedad es el nivel de escritura de la granularidad de bloqueo; si se puede escribir más de una propiedad para representar el estado de una transacción (por ej .: pedidos totales y peso total), entonces podría ser mejor tener el bloqueo en el nivel MyObject (es decir, lock (myObject.SyncRoot) ... .)
En el ejemplo de código que publicó, nunca se realiza un get.
En un ejemplo más complicado:
MyProperty.Field1 = MyProperty.doSomething() + 2;
Y, por supuesto, suponiendo que hiciste un:
lock (mLock)
{
// stuff...
}
En doSomething()
entonces todas las llamadas de bloqueo no serían suficientes para garantizar la sincronización en todo el objeto. Tan pronto como la función doSomething()
retorna, el bloqueo se pierde, luego se realiza la adición y luego ocurre la asignación, que se bloquea nuevamente.
O, para escribirlo de otra manera, puede pretender que los bloqueos no se realizan de forma automática, y reescribir esto más como "código de máquina" con una operación por línea, y se vuelve obvio:
lock (mLock)
{
val = doSomething()
}
val = val + 2
lock (mLock)
{
MyProperty.Field1 = val
}
En su versión editada, todavía no proporciona una forma segura de actualizar MyObject. Cualquier cambio en las propiedades del objeto tendrá que hacerse dentro de un bloque sincronizado / bloqueado.
Puede escribir facilitadores individuales para manejar esto, pero ha indicado que esto será difícil debido a los campos de número grande. Si de hecho es el caso (y aún no ha proporcionado suficiente información para evaluar esto), una alternativa es escribir un colocador que use la reflexión; esto le permitiría pasar una cadena que representa el nombre del campo, y podría buscar dinámicamente el nombre del campo y actualizar el valor. Esto le permitiría tener un setter único que funcionaría en cualquier cantidad de campos. Esto no es tan fácil ni tan eficiente, pero le permitiría tratar con una gran cantidad de clases y campos.
Ha implementado un bloqueo para obtener / configurar el objeto, pero no ha hecho seguro el hilo del objeto, que es otra historia.
He escrito un artículo sobre clases de modelos inmutables en C # que podría ser interesante en este contexto: http://rickyhelgesson.wordpress.com/2012/07/17/mutable-or-immutable-in-a-parallel-world/
La belleza del multihilo es que no sabes en qué orden van a pasar las cosas. Si configuras algo en un hilo, podría suceder primero, podría suceder después del intento.
El código que has publicado bloquea el miembro mientras se lee y escribe. Si desea manejar el caso donde se actualiza el valor, quizás deba buscar otras formas de sincronización, como events . (Vea las versiones automática / manual). Luego puede decirle a su hilo de "sondeo" que el valor ha cambiado y que está listo para ser releído.
La programación concurrente sería bastante fácil si tu enfoque pudiese funcionar. Pero no es así, el iceberg que hunde que Titanic es, por ejemplo, el cliente de tu clase que hace esto:
objectRef.MyProperty += 1;
La carrera de lectura-modificación-escritura es bastante obvia, hay peores. No hay absolutamente nada que pueda hacer para que su propiedad sea segura para hilos, a menos que sea inmutable. Es su cliente quien necesita lidiar con el dolor de cabeza. Ser obligado a delegar ese tipo de responsabilidad a un programador que es menos probable que lo haga bien es el talón de Aquiles de la programación concurrente.
No, su código no bloqueará el acceso a los miembros del objeto devuelto por MyProperty
. Solo bloquea MyProperty
sí mismo.
Su uso de ejemplo es realmente dos operaciones en uno, más o menos equivalente a esto:
// object is locked and then immediately released in the MyProperty getter
MyObject o = MyProperty;
// this assignment isn''t covered by a lock
o.Field1 = 2;
// the MyProperty setter is never even called in this example
En pocas palabras: si dos subprocesos acceden a MyProperty
simultáneamente, el getter bloqueará brevemente el segundo subproceso hasta que devuelva el objeto al primer subproceso, pero luego devolverá el objeto al segundo subproceso también. Ambos hilos tendrán acceso completo y desbloqueado al objeto.
EDITAR en respuesta a más detalles en la pregunta
Todavía no estoy 100% seguro de lo que estás tratando de lograr, pero si solo quieres acceso atómico al objeto, ¿no podrías tener el bloqueo del código de llamada contra el objeto mismo?
// quick and dirty example
// there''s almost certainly a better/cleaner way to do this
lock (MyProperty)
{
// other threads can''t lock the object while you''re in here
MyProperty.Field1 = 2;
// do more stuff if you like, the object is all yours
}
// now the object is up-for-grabs again
No es ideal, pero siempre que todo el acceso al objeto esté contenido en las secciones de lock (MyProperty)
, este enfoque será seguro para subprocesos.