153

我刚刚意识到,在我的代码中的某个地方,我在锁内有 return 语句,有时在外面。哪一个是最好的?

1)

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

2)

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

}

我应该使用哪一个?

4

9 回答 9

205

从本质上讲,无论哪种方式都使代码更简单。单点退出是一个不错的理想,但我不会为了实现它而将代码变形......如果替代方案是声明一个局部变量(在锁外),初始化它(在锁内)和然后将其返回(在锁外),然后我会说锁内的简单“返回 foo”要简单得多。

为了显示 IL 中的差异,让我们编写代码:

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

(请注意,我很乐意认为这ReturnInside是 C# 的一个更简单/更干净的位)

并查看 IL(发布模式等):

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

因此,在 IL 级别,它们 [给出或取一些名称] 相同(我学到了一些东西 ;-p)。因此,唯一合理的比较是本地编码风格的(高度主观的)法则......我更喜欢ReturnInside简单,但我不会对此感到兴奋。

于 2008-11-05T21:16:25.310 回答
44

它没有任何区别;它们都被编译器翻译成同一个东西。

为了澄清,任何一个都被有效地翻译成具有以下语义的东西:

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

return myData;
于 2008-11-05T21:11:45.460 回答
32

我肯定会把退货放在锁里。否则,您可能会冒另一个线程进入锁并在 return 语句之前修改您的变量的风险,从而使原始调用者收到与预期不同的值。

于 2008-11-07T17:59:43.943 回答
5

如果认为外面的锁看起来更好,但如果您最终将代码更改为:

return f(...)

如果 f() 需要在持有锁的情况下调用,那么它显然需要在锁内,因为这样将返回值保持在锁内以保持一致性是有意义的。

于 2008-11-05T21:15:26.513 回答
5

这取决于,

我要在这里反对谷物。我通常会回到锁内。

通常变量 mydata 是一个局部变量。我喜欢在初始化局部变量时声明它们。我很少有数据在我的锁之外初始化我的返回值。

所以你的比较实际上是有缺陷的。虽然理想情况下这两个选项之间的差异会像您所写的那样,这似乎是对案例 1 的认可,但实际上它有点丑陋。

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

对比

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

我发现案例 2 更容易阅读,也更难搞砸,尤其是对于短片段。

于 2008-11-05T21:25:50.533 回答
2

值得一提的是,MSDN 上的文档有一个从锁内部返回的示例。从这里的其他答案来看,它确实与 IL 非常相似,但对我来说,从锁内部返回似乎更安全,因为这样你就不会冒返回变量被另一个线程覆盖的风险。

于 2013-06-24T19:55:34.437 回答
0

为了让其他开发人员更容易阅读代码,我建议第一个替代方案。

于 2008-11-05T21:14:24.423 回答
0

lock() return <expression>陈述总是:

1)进入锁

2) 为指定类型的值进行本地(线程安全)存储,

3) 用返回的值填充存储<expression>

4) 退出锁

5) 退货。

这意味着从 lock 语句返回的值在返回之前总是“煮熟”。

不要担心lock() return,不要在这里听任何人))

于 2017-12-27T16:19:14.603 回答
-2

注意:我相信这个答案实际上是正确的,我希望它也有帮助,但我总是很乐意根据具体的反馈来改进它。

总结和补充现有答案:

  • 接受的答案表明,无论您在C#代码中选择哪种语法形式,在 IL 代码中 - 因此在运行时 -直到锁被释放return才会发生。

    • 因此,即使将块放在块return 内部lock,严格来说,也歪曲了控制流[1],但它在语法上很方便,因为它不需要将返回值存储在 aux 中。局部变量(在块外声明,以便可以与return块外的 a 一起使用) - 请参阅Edward KMETT 的回答
  • 另外-这方面是问题的附带因素,但可能仍然令人感兴趣(里卡多·维拉米尔的回答试图解决它,但我认为是错误的)-将lock语句与语句结合起来-即,在受保护的块中return获取价值return并发访问 - 只有在调用者的范围内实际上不需要保护时才有意义地“保护”返回值,这适用于以下场景:

    • 如果返回值是集合中的一个元素,它只需要在添加和删除元素方面进行保护,而不是在元素本身的修改和/或...

    • ...如果返回的值是值类型字符串的实例。

      • 请注意,在这种情况下,调用者会收到该值的快照(副本)[2] - 到调用者检查时,它可能不再是原始数据结构中的当前值。
    • 在任何其他情况下,锁定必须由调用者执行,而不是(仅)在方法内部执行。


[1] Theodor Zoulias指出,从技术上讲,这对于放置returntry, catch, using, if, while, for, ... 语句中也是正确的;但是,该lock声明的特定目的可能会引起对真正控制流的审查,这一点已被提出并受到了很多关注,就证明了这一点。

[2] 访问一个值类型实例总是会创建一个线程本地的、堆栈上的副本;尽管字符串在技术上是引用类型实例,但它们实际上表现得像值类型实例。

于 2020-05-26T18:18:16.357 回答