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

163
我刚刚意识到在我的代码中,有些地方return语句在锁的内部,有些则在外部。哪种方式最好?
void example()
{
    lock (mutex)
    {
    //...
    }
    return myData;
}
void example()
{
    lock (mutex)
    {
    //...
    return myData;
    }

}

我应该使用哪一个?


如何尝试解除Reflector并进行一些IL比较呢?;-) - Pop Catalin
7
@Pop: 完成 - 在IL层面上,两者都没有更好的选择 - 只有C#风格适用。 - Marc Gravell
1
非常有趣,哇,我今天学到了东西! - Pokus
@PopCatalin,很抱歉问这个问题,但是“IL”和Reflector是什么? - Vandrey
@Sunburst275:看一下 https://www.microsoft.com/en-us/p/ilspy/9mxfbkfvsq13?activetab=pivot:overviewtab - Peter
9个回答

217

基本上,让代码变得更加简单的方式就是最好的方式。单一的出口点是一个很好的理想,但我不会为了达到这个目标而弯曲代码... 如果替代方案是声明一个局部变量(在锁之外),初始化它(在锁内)然后返回它(在锁之外),那么我认为在锁内简单地使用“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来保持简单,但我对任何一个都不会感到兴奋。


16
我使用了(免费且出色的)Red Gate公司的.NET Reflector(之前是Lutz Roeder的.NET Reflector),但ILDASM也可以。 - Marc Gravell
1
Reflector 最强大的功能之一是您可以将 IL 反编译为您喜欢的语言(如 C#、VB、Delphi、MC ++、Chrome 等)。 - Marc Gravell
3
针对您的简单示例,IL保持不变,但这可能是因为您只返回了一个恒定值?!我相信在实际场景中,结果可能会有所不同,并且并行线程可能会通过修改值来干扰彼此,在锁块之外返回语句可能存在风险! - Torbjørn
1
@RaheelKhan 不是的,它们是相同的。在 IL 级别上,你不能.try 区域内使用 ret - Marc Gravell
@Torbjørn 我认为你的担忧是有道理的,但只有在 ProgramReturnInsideReturnOutside 声明为静态时才会出现问题,否则 i 就是一个局部变量,是安全的。 - ViRuSTriNiTy
显示剩余4条评论

44

这并没有什么区别;它们都被编译器翻译成相同的内容。

为了澄清,两者都被有效地翻译成具有以下语义的内容:

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

return myData;

2
这对于try/finally是正确的 - 然而,在锁之外的返回仍然需要额外的本地变量,这些变量无法被优化掉 - 并且需要更多的代码... - Marc Gravell
3
在 try 块中不能使用 return 语句,而必须以 ".leave" 操作码结束。因此,无论哪种情况,生成的 CIL 应该是相同的。 - Greg Beech
4
你是正确的 - 我刚刚查看了IL(请参见更新的帖子)。我学到了一些东西;-p - Marc Gravell
2
很酷,不幸的是我从痛苦的几个小时中学到了在try块中发出.ret操作码并且CLR拒绝加载我的动态方法的教训 :-( - Greg Beech
我能理解;我做过相当数量的Reflection.Emit,但我很懒;除非我非常确定某件事情,否则我会用C#编写代表性代码然后查看IL。但令人惊讶的是你开始很快以IL术语思考(即堆栈序列)。 - Marc Gravell

35

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


4
其他回答者似乎忽略了这一点,但这是正确的。他们所创建的简单示例可能会产生相同的IL代码,但在大多数实际场景中并非如此。 - Torbjørn
5
其他回答没有谈到这点,我感到很惊讶。 - Akshat Agarwal
5
在这个例子中,他们在讨论使用栈变量来存储返回值,也就是说,只有返回语句在锁之外以及变量声明。另一个线程应该有另一个栈,因此不会造成任何危害,我是对的吗? - Guillermo Ruffino
3
我认为这不是一个有效的观点,因为在返回调用和将返回值实际赋值给主线程变量之间,另一个线程可能会更新该值。无论如何,返回的值都不能被更改或保证与当前实际值一致。对吗? - Uroš Joksimović
2
这个答案是不正确的。另一个线程无法更改局部变量。局部变量存储在堆栈中,每个线程都有自己的堆栈。顺便说一下,线程堆栈的默认大小为[1 MB](https://dev59.com/Y14b5IYBdhLWcg3wzUja)。 - Theodor Zoulias
显示剩余3条评论

5

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

return f(...)

如果需要在锁定状态下调用f(),那么显然需要将其放在锁内部。因此,为了保持一致性,将返回语句也放在锁内是有意义的。

5

这要看情况而定。

我会与众不同地建议在锁内返回值。

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

因此,你的比较实际上是有缺陷的。虽然理想情况下,两个选项之间的区别应该像你所写的那样,看起来倾向于第一种情况,但实际上情况要复杂得多。

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

vs.

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

我认为对于短代码片段来说,第二种情况更易于阅读且更难出错。


2

值得一提的是,MSDN文档中有一个从锁内返回的示例。从其他答案来看,它似乎是非常类似IL的语法,但对我来说,在锁内返回似乎更加安全,因为这样可以避免另一个线程覆盖返回变量。


1

lock() return <expression>语句总是:

1)进入锁定状态

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

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

4)退出锁定状态

5)返回存储器。

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

不要担心lock() return,不要听任何人的建议))


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

-1

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

总结和补充现有答案:

接受的答案表明,无论您在C#代码中选择哪种语法形式,在IL代码中(因此在运行时),return都不会发生,直到释放锁定时才会发生。 尽管将return放置在lock块内因此严格来说误导了控制流程[1],但从语法上讲,在块内使用return可以方便地避免需要在块外声明一个辅助局部变量(以便可以在块外使用return)以存储返回值-请参见Edward KMETT的答案。 另外-这个方面与问题无关,但可能仍然很有趣(Ricardo Villamil的答案试图解决它,但我认为是错误的)-结合lock语句和return语句-即,在受并发访问保护的块中获得值以return-只有在实际上不需要保护一旦获得就会"保护"调用者范围内的返回值的情况下才有意义。这适用于以下情况: 如果返回的值是来自仅需要在添加和删除元素方面进行保护而不需要在元素本身的修改方面进行保护的集合中的元素... ...如果返回的值是值类型或字符串的实例。 请注意,在这种情况下,调用者接收到该值的快照(拷贝)[2] - 在调用者检查该值时,它可能不再是原始数据结构中的当前值。 在任何其他情况下,锁定必须由调用者执行,而不是(仅)在方法内执行。

[1] Theodor Zoulias 指出,在 trycatchusingifwhilefor 等语句中放置 return 也是技术上正确的;然而,lock 语句的特定目的很可能会引起对真实控制流的审查,正如这个问题已经被问到并得到了很多关注。

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


1
关于您回答的当前状态(第13个修订版本),您仍在推测lock存在的原因,并从返回语句的位置中得出含义。这是与此问题无关的讨论,在我看来。此外,我发现使用“误导”这个词相当令人不安。如果从lock返回会误导控制流,则同样可以说从trycatchusingifwhilefor和语言的任何其他结构返回也是如此。这就像说C#充满了控制流误导一样。天哪... - Theodor Zoulias
这就像说 C# 充斥着控制流错误表述。嗯,从技术上讲这是正确的,如果你选择这样理解,“错误表述”只是一个价值判断。使用 try、if 等语句时,我个人很少去考虑它,但在 lock 的上下文中,这个问题引起了我的关注 - 如果其他人没有遇到同样的问题,这个问题就不会被提出,并且被接受的答案也不会费尽心思地调查真正的行为。 - mklement0
这个答案现在 - 间接地 - 正在Meta SO上讨论。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接