¿Debe una declaración de devolución estar dentro o fuera de una cerradura?

142

Me acabo de dar cuenta de que en algún lugar de mi código tengo la declaración de devolución dentro de la cerradura y en algún momento afuera. ¿Cuál es el mejor?

1)

void example()
{
    lock (mutex)
    {
    //...
    }
    return myData;
}

2)

void example()
{
    lock (mutex)
    {
    //...
    return myData;
    }

}

¿Cuál debo usar?

Patrick Desjardins
fuente
¿Qué tal disparar Reflector y hacer alguna comparación IL ;-).
Pop Catalin
66
@Pop: hecho - ninguno es mejor en términos de IL - solo se aplica el estilo C #
Marc Gravell
1
Muy interesante, ¡wow, aprendo algo hoy!
Pokus

Respuestas:

192

Esencialmente, lo que hace que el código sea más simple. El único punto de salida es un buen ideal, pero no doblaría el código solo para lograrlo ... Y si la alternativa es declarar una variable local (fuera de la cerradura), inicializarla (dentro de la cerradura) y luego devolverlo (fuera de la cerradura), luego diría que un simple "retorno foo" dentro de la cerradura es mucho más simple.

Para mostrar la diferencia en IL, permite codificar:

static class Program
{
    static void Main() { }

    static readonly object sync = new object();

    static int GetValue() { return 5; }

    static int ReturnInside()
    {
        lock (sync)
        {
            return GetValue();
        }
    }

    static int ReturnOutside()
    {
        int val;
        lock (sync)
        {
            val = GetValue();
        }
        return val;
    }
}

(tenga en cuenta que felizmente argumentaría que ReturnInsidees un bit más simple / más limpio de C #)

Y mira el IL (modo de lanzamiento, etc.):

.method private hidebysig static int32 ReturnInside() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 CS$1$0000,
        [1] object CS$2$0001)
    L_0000: ldsfld object Program::sync
    L_0005: dup 
    L_0006: stloc.1 
    L_0007: call void [mscorlib]System.Threading.Monitor::Enter(object)
    L_000c: call int32 Program::GetValue()
    L_0011: stloc.0 
    L_0012: leave.s L_001b
    L_0014: ldloc.1 
    L_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_001a: endfinally 
    L_001b: ldloc.0 
    L_001c: ret 
    .try L_000c to L_0014 finally handler L_0014 to L_001b
} 

method private hidebysig static int32 ReturnOutside() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 val,
        [1] object CS$2$0000)
    L_0000: ldsfld object Program::sync
    L_0005: dup 
    L_0006: stloc.1 
    L_0007: call void [mscorlib]System.Threading.Monitor::Enter(object)
    L_000c: call int32 Program::GetValue()
    L_0011: stloc.0 
    L_0012: leave.s L_001b
    L_0014: ldloc.1 
    L_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
    L_001a: endfinally 
    L_001b: ldloc.0 
    L_001c: ret 
    .try L_000c to L_0014 finally handler L_0014 to L_001b
}

Entonces, en el nivel de IL son [dar o tomar algunos nombres] idénticos (aprendí algo ;-p). Como tal, la única comparación sensata es la ley (altamente subjetiva) del estilo de codificación local ... Prefiero la ReturnInsidesimplicidad, pero tampoco me entusiasmaría.

Marc Gravell
fuente
15
Utilicé el reflector .NET (gratuito y excelente) de Red Gate (era: el reflector .NET de Lutz Roeder), pero ILDASM también lo haría.
Marc Gravell
1
Uno de los aspectos más poderosos de Reflector es que realmente puede desmontar IL en su idioma preferido (C #, VB, Delphi, MC ++, Chrome, etc.)
Marc Gravell
3
Para su ejemplo simple, el IL se mantiene igual, ¿pero eso es probablemente porque solo devuelve un valor constante? Creo que para escenarios de la vida real, el resultado puede diferir, y los subprocesos paralelos pueden causar problemas el uno al otro modificando el valor antes de devolverlo, la declaración de devolución está fuera del bloque de bloqueo. ¡Peligroso!
Torbjørn
@MarcGravell: Acabo de encontrar tu publicación mientras reflexionaba sobre lo mismo e incluso después de leer tu respuesta, todavía no estoy seguro de lo siguiente: ¿Hay ALGUNA circunstancia en la que usar el enfoque externo podría romper la lógica segura para subprocesos? Pregunto esto ya que prefiero un único punto de retorno y no me 'SIENTO' bien por su seguridad de hilo. Aunque, si la IL es la misma, mi preocupación debería ser discutible de todos modos.
Raheel Khan
1
@RaheelKhan no, ninguno; Ellos son iguales. En el nivel IL, no puedes ret dentro de una .tryregión.
Marc Gravell
42

No hace ninguna diferencia; ambos están traducidos a lo mismo por el compilador.

Para aclarar, cualquiera se traduce efectivamente a algo con la siguiente semántica:

T myData;
Monitor.Enter(mutex)
try
{
    myData= // something
}
finally
{
    Monitor.Exit(mutex);
}

return myData;
Greg Beech
fuente
1
Bueno, eso es cierto para el intento / finalmente, sin embargo, el regreso fuera de la cerradura todavía requiere locales adicionales que no se pueden optimizar, y requiere más código ...
Marc Gravell
3
No puede regresar de un bloque de prueba; debe terminar con un código operativo ".leave". Entonces, el CIL emitido debe ser el mismo en cualquier caso.
Greg Beech
3
Tienes razón: acabo de mirar el IL (ver publicación actualizada). Aprendí algo ;-p
Marc Gravell
2
Genial, desafortunadamente aprendí de horas dolorosas tratando de emitir códigos de operación .ret en bloques de prueba y haciendo que el CLR se niegue a cargar mis métodos dinámicos :-(
Greg Beech
Puedo relacionar; He hecho una buena cantidad de Reflection.Emit, pero soy vago; a menos que esté muy seguro de algo, escribo un código representativo en C # y luego miro el IL. Pero es sorprendente lo rápido que comienza a pensar en términos IL (es decir, secuenciar la pila).
Marc Gravell
28

Definitivamente pondría la devolución dentro de la cerradura. De lo contrario, corre el riesgo de que otro subproceso ingrese al bloqueo y modifique su variable antes de la declaración de devolución, por lo tanto, la persona que llama original recibe un valor diferente al esperado.

Ricardo Villamil
fuente
44
Esto es correcto, un punto que los otros respondedores parecen estar perdiendo. Las muestras simples que han hecho pueden producir la misma IL, pero esto no es así para la mayoría de los escenarios de la vida real.
Torbjørn
44
Estoy sorprendido de que las otras respuestas no hablen de esto
Akshat Agarwal
55
En esta muestra están hablando de usar una variable de pila para almacenar el valor de retorno, es decir, solo la declaración de retorno fuera del bloqueo y, por supuesto, la declaración de la variable. Otro hilo debería tener otra pila y, por lo tanto, no podría causar ningún daño, ¿estoy en lo cierto?
Guillermo Ruffino
3
No creo que este sea un punto válido, ya que otro hilo podría actualizar el valor entre la llamada de retorno y la asignación real del valor de retorno a la variable en el hilo principal. El valor que se devuelve no puede modificarse ni garantizarse la coherencia con el valor real actual de ninguna manera. ¿Correcto?
Uroš Joksimović
Esta respuesta es incorrecta. Otro hilo no puede cambiar una variable local. Las variables locales se almacenan en la pila, y cada subproceso tiene su propia pila. Por cierto, el tamaño predeterminado de la pila de un hilo es de 1 MB .
Theodor Zoulias
5

Depende,

Voy a ir contra la corriente aquí. En general, volvería dentro de la cerradura.

Por lo general, la variable mydata es una variable local. Soy aficionado a declarar variables locales mientras las inicializo. Raramente tengo los datos para inicializar mi valor de retorno fuera de mi candado.

Entonces su comparación es realmente defectuosa. Si bien, idealmente, la diferencia entre las dos opciones sería la que había escrito, lo que parece indicar el caso 1, en la práctica es un poco más feo.

void example() { 
    int myData;
    lock (foo) { 
        myData = ...;
    }
    return myData
}

vs.

void example() { 
    lock (foo) {
        return ...;
    }
}

Creo que el caso 2 es considerablemente más fácil de leer y más difícil de arruinar, especialmente para fragmentos cortos.

Edward KMETT
fuente
4

Si cree que la cerradura exterior se ve mejor, pero tenga cuidado si termina cambiando el código a:

return f(...)

Si es necesario invocar f () con el candado retenido, entonces obviamente debe estar dentro del candado, por lo que mantener los retornos dentro del candado para mantener la coherencia tiene sentido.

Rob Walker
fuente
1

Para lo que vale, la documentación en MSDN tiene un ejemplo de regreso desde el interior de la cerradura. De las otras respuestas aquí, parece ser una IL bastante similar, pero, para mí, parece más seguro regresar desde el interior de la cerradura porque entonces no corres el riesgo de que una variable de retorno sea sobrescrita por otro hilo.

greyseal96
fuente
0

Para facilitar a los demás desarrolladores la lectura del código, sugeriría la primera alternativa.

Adam Asham
fuente
0

lock() return <expression> declaraciones siempre:

1) entrar cerradura

2) crea una tienda local (segura para subprocesos) para el valor del tipo especificado,

3) llena la tienda con el valor devuelto por <expression>,

4) cerradura de salida

5) devolver la tienda.

Significa que el valor, devuelto por la declaración de bloqueo, siempre "cocido" antes de la devolución.

No te preocupes lock() return, no escuches a nadie aquí))

mshakurov
fuente
-2

Nota: Creo que esta respuesta es objetivamente correcta y espero que también sea útil, pero siempre estoy feliz de mejorarla en base a comentarios concretos.

Para resumir y complementar las respuestas existentes:

  • La respuesta aceptada muestra que, independientemente de la forma de sintaxis que elija en su código C # , en el código IL, y por lo tanto en tiempo de ejecución, returnno sucede hasta después de que se libera el bloqueo.

    • Aunque colocar el return interior del lockbloque por lo tanto, estrictamente hablando, tergiversa el flujo de control [1] , es sintácticamente conveniente porque obvia la necesidad de almacenar el valor de retorno en un auxiliar. variable local (declarada fuera del bloque, para que pueda usarse con un returnfuera del bloque) - vea la respuesta de Edward KMETT .
  • Por separado, y este aspecto es incidental a la pregunta, pero aún puede ser de interés ( la respuesta de Ricardo Villamil intenta abordarlo, pero creo que es incorrecto), combinando una lockdeclaración con una returndeclaración, es decir, obteniendo valor returnen un bloque protegido de acceso concurrente: solo "protege" significativamente el valor devuelto en el alcance de la persona que llama si realmente no necesita protección una vez obtenido , lo que se aplica en los siguientes escenarios:

    • Si el valor devuelto es un elemento de una colección que solo necesita protección en términos de agregar y eliminar elementos, no en términos de modificaciones de los propios elementos y / o ...

    • ... si el valor que se devuelve es una instancia de un tipo de valor o una cadena .

      • Tenga en cuenta que, en este caso, la persona que llama recibe una instantánea (copia) [2] del valor, que para cuando la persona que realiza la inspección ya no sea el valor actual en la estructura de datos de origen.
    • En cualquier otro caso, el bloqueo debe ser realizado por la persona que llama , no (solo) dentro del método.


[1] Theodor Zoulias señala que esto es técnicamente cierto también para la colocación returnen el interior try, catch, using, if, while, for, ... declaraciones; sin embargo, es el propósito específico de la lockdeclaración lo que probablemente invitará al escrutinio del verdadero flujo de control, como lo demuestra esta pregunta que se hizo y recibió mucha atención.

[2] Al acceder a una instancia de tipo de valor, invariablemente se crea una copia local en la pila de subprocesos; aunque las cadenas son instancias de tipo de referencia técnica, se comportan efectivamente como instancias de tipo de valor similar.

mklement0
fuente
Con respecto al estado actual de su respuesta (revisión 13), todavía está especulando sobre las razones de la existencia de la respuesta locky derivando el significado de la ubicación de la declaración de devolución. Que es una discusión no relacionada con esta pregunta en mi humilde opinión. También encuentro el uso de "tergiversaciones" bastante inquietante. Si volver de un locktergiversa el flujo de control, entonces el mismo puede decirse para el retorno de una try, catch, using, if, while, for, y cualquier otra construcción de la lengua. Es como decir que C # está plagado de tergiversaciones de flujo de control. Jesús ...
Theodor Zoulias
"Es como decir que C # está plagado de tergiversaciones de flujo de control". Bueno, eso es técnicamente cierto, y el término "tergiversación" es solo un juicio de valor si eliges tomarlo de esa manera. Con try, if, ... yo personalmente no tienden a pensar en ello, pero en el contexto de lock, en concreto, se planteó la cuestión para mí - y si no se plantea también para los demás, nunca habría hecho esta pregunta , y la respuesta aceptada no habría ido demasiado lejos para investigar el verdadero comportamiento.
mklement0