Une déclaration de retour doit-elle être à l'intérieur ou à l'extérieur d'une serrure?

142

Je viens de me rendre compte qu'à un endroit de mon code, j'ai l'instruction return à l'intérieur de la serrure et parfois à l'extérieur. Lequel est le meilleur?

1)

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

2)

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

}

Lequel dois-je utiliser?

Patrick Desjardins
la source
Que diriez-vous de déclencher Reflector et de faire une comparaison IL ;-).
Pop Catalin
6
@Pop: done - ni l'un ni l'autre n'est meilleur en termes d'IL - seul le style C # s'applique
Marc Gravell
1
Très intéressant, wow j'apprends quelque chose aujourd'hui!
Pokus

Réponses:

192

Essentiellement, ce qui rend le code plus simple. Le point de sortie unique est un bel idéal, mais je ne déformerais pas le code juste pour y parvenir ... Et si l'alternative est de déclarer une variable locale (en dehors du verrou), de l'initialiser (à l'intérieur du verrou) et puis le renvoyer (à l'extérieur de la serrure), alors je dirais qu'un simple "return foo" à l'intérieur de la serrure est beaucoup plus simple.

Pour montrer la différence dans IL, permet de coder:

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;
    }
}

(notez que je soutiendrais volontiers que ReturnInsidec'est un peu plus simple / plus propre de C #)

Et regardez l'IL (mode de libération, 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
}

Donc au niveau IL ils sont [donner ou prendre quelques noms] identiques (j'ai appris quelque chose ;-p). En tant que tel, la seule comparaison raisonnable est la loi (très subjective) du style de codage local ... Je préfère ReturnInsidepour la simplicité, mais je ne serais pas excité non plus.

Marc Gravell
la source
15
J'ai utilisé le réflecteur .NET (gratuit et excellent) de Red Gate (était: le réflecteur .NET de Lutz Roeder), mais ILDASM le ferait aussi.
Marc Gravell
1
L'un des aspects les plus puissants de Reflector est que vous pouvez réellement désassembler IL dans votre langage préféré (C #, VB, Delphi, MC ++, Chrome, etc.)
Marc Gravell
3
Pour votre exemple simple, l'IL reste le même, mais c'est probablement parce que vous ne renvoyez qu'une valeur constante?! Je crois que pour les scénarios réels, le résultat peut différer, et les threads parallèles peuvent causer des problèmes les uns pour les autres en modifiant la valeur avant qu'elle ne soit renvoyée, l'instruction de retour est en dehors du bloc de verrouillage. Dangereux!
Torbjørn
@MarcGravell: Je viens de tomber sur votre message en réfléchissant à la même chose et même après avoir lu votre réponse, je ne suis toujours pas sûr de ce qui suit: Y a-t-il des circonstances où l'utilisation de l'approche externe pourrait briser la logique thread-safe. Je pose cette question car je préfère un point de retour unique et je ne me sens pas bien sur sa sécurité de filetage. Bien que, si l'IL est le même, ma préoccupation devrait de toute façon être sans objet.
Raheel Khan
1
@RaheelKhan non, aucun; ce sont les mêmes. Au niveau IL, vous ne pouvez pas à l' ret intérieur d'une .tryrégion.
Marc Gravell
42

Cela ne fait aucune différence; ils sont tous deux traduits de la même manière par le compilateur.

Pour clarifier, l'un ou l'autre est effectivement traduit en quelque chose avec la sémantique suivante:

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

return myData;
Greg Beech
la source
1
Eh bien, c'est vrai pour try / finally - cependant, le retour à l'extérieur de l'écluse nécessite toujours des locaux supplémentaires qui ne peuvent pas être optimisés - et prend plus de code ...
Marc Gravell
3
Vous ne pouvez pas revenir d'un bloc try; il doit se terminer par un op-code ".leave". Le CIL émis doit donc être le même dans les deux cas.
Greg Beech
3
Vous avez raison - je viens de regarder l'IL (voir l'article mis à jour). J'ai appris quelque chose ;-p
Marc Gravell
2
Cool, malheureusement, j'ai appris des heures douloureuses en essayant d'émettre des op-codes .ret dans des blocs d'essai et en demandant au CLR de refuser de charger mes méthodes dynamiques :-(
Greg Beech
Je comprends; J'ai fait pas mal de Reflection.Emit, mais je suis paresseux; sauf si je suis très sûr de quelque chose, j'écris du code représentatif en C #, puis je regarde l'IL. Mais il est surprenant de voir à quelle vitesse vous commencez à penser en termes IL (c'est-à-dire séquencer la pile).
Marc Gravell
28

Je mettrais certainement le retour dans la serrure. Sinon, vous risquez qu'un autre thread entre dans le verrou et modifie votre variable avant l'instruction de retour, ce qui fait que l'appelant d'origine reçoit une valeur différente de celle attendue.

Ricardo Villamil
la source
4
C'est exact, un point qui semble manquer aux autres intervenants. Les échantillons simples qu'ils ont réalisés peuvent produire le même IL, mais ce n'est pas le cas pour la plupart des scénarios réels.
Torbjørn
4
Je suis surpris que les autres réponses n'en parlent pas
Akshat Agarwal
5
Dans cet exemple, ils parlent d'utiliser une variable de pile pour stocker la valeur de retour, c'est-à-dire uniquement l'instruction de retour en dehors du verrou et bien sûr la déclaration de variable. Un autre thread devrait avoir une autre pile et ne pourrait donc pas faire de mal, n'est-ce pas?
Guillermo Ruffino
3
Je ne pense pas que ce soit un point valide, car un autre thread pourrait mettre à jour la valeur entre l'appel de retour et l'affectation réelle de la valeur de retour à la variable sur le thread principal. La valeur renvoyée ne peut pas être modifiée ni être garantie de cohérence avec la valeur réelle actuelle dans les deux cas. Droite?
Uroš Joksimović
Cette réponse est incorrecte. Un autre thread ne peut pas modifier une variable locale. Les variables locales sont stockées dans la pile et chaque thread a sa propre pile. BTW la taille par défaut de la pile d'un thread est de 1 Mo .
Theodor Zoulias
5

Ça dépend,

Je vais aller à contre-courant ici. Je reviendrais généralement à l'intérieur de la serrure.

Habituellement, la variable mydata est une variable locale. J'aime déclarer des variables locales pendant que je les initialise. J'ai rarement les données pour initialiser ma valeur de retour en dehors de mon verrou.

Donc, votre comparaison est en fait erronée. Bien que, idéalement, la différence entre les deux options soit celle que vous aviez écrite, ce qui semble donner un clin d'œil au cas 1, en pratique, c'est un peu plus laid.

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

contre.

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

Je trouve que le cas 2 est considérablement plus facile à lire et plus difficile à bousiller, en particulier pour les courts extraits.

Edward KMETT
la source
4

Si vous pensez que la serrure à l'extérieur est meilleure, mais faites attention si vous finissez par changer le code en:

return f(...)

Si f () doit être appelé avec le verrou maintenu, il doit évidemment être à l'intérieur du verrou, car il est logique de conserver les retours à l'intérieur du verrou pour la cohérence.

Rob Walker
la source
1

Pour ce que ça vaut, la documentation sur MSDN a un exemple de retour de l'intérieur de la serrure. D'après les autres réponses ici, cela semble être assez similaire à IL mais, pour moi, il semble plus sûr de revenir de l'intérieur du verrou, car vous ne courez pas le risque qu'une variable de retour soit écrasée par un autre thread.

greyseal96
la source
0

Pour faciliter la lecture du code par les autres développeurs, je suggère la première alternative.

Adam Asham
la source
0

lock() return <expression> déclarations toujours:

1) entrer dans la serrure

2) rend le stockage local (thread-safe) pour la valeur du type spécifié,

3) remplit le magasin avec la valeur renvoyée par <expression>,

4) Verrouillage de sortie

5) retourner le magasin.

Cela signifie que la valeur, renvoyée par l'instruction de verrouillage, est toujours "cuite" avant le retour.

Ne t'inquiète lock() returnpas, n'écoute personne ici))

Mshakurov
la source
-2

Remarque: je pense que cette réponse est factuellement correcte et j'espère qu'elle est également utile, mais je suis toujours heureux de l'améliorer sur la base de commentaires concrets.

Pour résumer et compléter les réponses existantes:

  • La réponse acceptée montre que, quelle que soit la forme de syntaxe que vous choisissez dans votre code C # , dans le code IL - et donc au moment de l'exécution - returncela ne se produit qu'après la libération du verrou.

    • Même si placer l' return intérieur du lockbloc par conséquent, à proprement parler, dénature le flux de contrôle [1] , il est syntaxiquement pratique en ce qu'il évite le besoin de stocker la valeur de retour dans un aux. variable locale (déclarée à l'extérieur du bloc, afin qu'elle puisse être utilisée avec une returnà l'extérieur du bloc) - voir la réponse d'Edward KMETT .
  • Séparément - et cet aspect est accessoire à la question, mais peut toujours être intéressant ( la réponse de Ricardo Villamil essaie d'y répondre, mais de manière incorrecte, je pense) - en combinant une lockdéclaration avec une returndéclaration - c'est-à-dire obtenir une valeur returndans un bloc protégé de accès simultané - "protège" de manière significative la valeur renvoyée dans la portée de l' appelant uniquement si elle n'a pas réellement besoin d'être protégée une fois obtenue , ce qui s'applique dans les scénarios suivants:

    • Si la valeur renvoyée est un élément d'une collection qui n'a besoin de protection qu'en termes d' ajout et de suppression d' éléments, pas en termes de modifications des éléments eux - mêmes et / ou ...

    • ... si la valeur renvoyée est une instance d'un type valeur ou d'une chaîne .

      • Notez que dans ce cas, l'appelant reçoit un instantané (copie) [2] de la valeur - qui au moment où l'appelant l'inspecte peut ne plus être la valeur courante dans la structure de données d'origine.
    • Dans tous les autres cas, le verrouillage doit être effectué par l' appelant , pas (seulement) à l'intérieur de la méthode.


[1] Theodor Zoulias souligne que ce qui est techniquement vrai aussi pour placer l' returnintérieur try, catch, using, if, while, for, ... déclarations; cependant, c'est le but spécifique de la lockdéclaration qui est susceptible d'inviter à un examen minutieux du véritable flux de contrôle, comme en témoigne cette question qui a été posée et a reçu beaucoup d'attention.

[2] L'accès à une instance de type valeur crée invariablement une copie locale de thread sur la pile; même si les chaînes sont techniquement des instances de type référence, elles se comportent effectivement comme des instances de type valeur similaire.

mklement0
la source
En ce qui concerne l'état actuel de votre réponse (révision 13), vous spéculez toujours sur les raisons de l'existence du lock, et tirez une signification du placement de la déclaration de retour. Ce qui est une discussion sans rapport avec cette question à mon humble avis. De plus, je trouve l'utilisation de "déformer" assez dérangeante. Si retour d'une lockmanière erronée le flux de contrôle, la même chose pourrait être dit pour le retour d'un try, catch, using, if, while, foret toute autre construction de la langue. C'est comme dire que le C # est truffé de fausses déclarations de flux de contrôle. Jesus ...
Theodor Zoulias
"C'est comme dire que le C # est criblé de fausses déclarations de flux de contrôle" - Eh bien, c'est techniquement vrai, et le terme "fausse déclaration" n'est qu'un jugement de valeur si vous choisissez de le prendre de cette façon. Avec try, if... je ne tends personnellement à y penser, mais dans le contexte de lock, en particulier, la question se posait pour moi - et si elle ne se posait pas pour les autres aussi, cette question n'aurais jamais été demandé , et la réponse acceptée ne serait pas allée très loin pour enquêter sur le vrai comportement.
mklement0