return语句应该在锁内部还是在锁外部

本文关键字:外部 内部 语句 return | 更新日期: 2023-09-27 17:48:48

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

1)

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

2)

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

我应该用哪一个?

return语句应该在锁内部还是在锁外部

本质上,它使代码变得更简单。单点退出是一个很好的理想选择,但我不会为了实现它而让代码变形……如果另一种选择是声明一个局部变量(在锁外),初始化它(在锁内),然后返回它(在锁定外),那么我会说,在锁内简单的"return 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,但我不会对此感到兴奋。

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

为了澄清,两者都可以有效地翻译为具有以下语义的东西:

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

我肯定会把返回放入锁中。否则,您将面临另一个线程进入锁并在return语句之前修改变量的风险,从而使原始调用方接收到与预期不同的值。

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

return f(...)

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

这取决于

我在这里要违背常理。我通常会回到锁里面。

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

所以你的比较实际上是有缺陷的。虽然理想情况下,这两种选择之间的区别就像你所写的那样,这似乎是对情况1的认可,但实际上它有点丑陋。

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

与。

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

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

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

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

lock() return <expression>语句始终:

1) 进入锁定

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

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

4) 退出锁定

5) 退货。

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

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

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

总结和补充现有答案:

  • 公认的答案表明,无论您在C#代码中选择哪种语法形式,在IL代码中——因此在运行时——return都不会发生,直到锁定释放后

    • 因此,即使将return放置在lock块内,严格地说,也会歪曲控制流[1],但在语法上很方便,因为它不需要将返回值存储在aux中。局部变量(在块外声明,以便它可以与块外的return一起使用)-请参阅Edward KMETT的答案
  • 单独地——这一方面是问题的附带,但可能仍然令人感兴趣(Ricardo Villamil的回答试图解决它,但我认为是错误的)——将lock语句与return语句相结合——即,在受保护的块中获得return的值,以避免并发访问——只有意义上的";保护";调用程序的作用域中的返回值,如果它在获得后实际上不需要保护,则该值适用于以下场景:

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

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

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


[1]Theodor Zoulias指出,将return放置在trycatchusingifwhilefor、。。。声明;然而,lock声明的具体目的可能会引起对真实控制流的仔细审查,这一问题已经被提出并受到了广泛关注

[2]访问值类型实例总是在其堆栈副本上创建一个线程本地;尽管字符串在技术上是引用类型实例,但它们的行为实际上类似于值类型实例