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

Posted

技术标签:

【中文标题】return 语句应该在锁的内部还是外部?【英文标题】:Should a return statement be inside or outside a lock? 【发布时间】:2010-09-20 23:14:41 【问题描述】:

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

1)

void example()

    lock (mutex)
    
    //...
    
    return myData;

2)

void example()

    lock (mutex)
    
    //...
    return myData;
    


我应该使用哪一个?

【问题讨论】:

如何启动 Reflector 并进行一些 IL 比较 ;-)。 @Pop: 完成 - 在 IL 方面两者都不是更好 - 仅适用 C# 样式 很有意思,哇今天学到东西了! @PopCatalin 不好意思问这个,什么是“IL”和 Reflector? @Sunburst275:看看microsoft.com/en-us/p/ilspy/… 【参考方案1】:

本质上,使代码更简单。单点退出是一个不错的理想,但我不会为了实现它而将代码变形......如果替代方案是声明一个局部变量(在锁外),初始化它(在锁内)和然后将其返回(在锁外),那么我会说锁内的简单“返回 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,但我也不会对此感到兴奋。

【讨论】:

我使用了(免费且优秀的)Red Gate 的 .NET Reflector(原为 Lutz Roeder 的 .NET Reflector),但 ILDASM 也可以。 Reflector 最强大的方面之一是您实际上可以将 IL 反汇编为您喜欢的语言(C#、VB、Delphi、MC++、Chrome 等) 对于您的简单示例,IL 保持不变,但这可能是因为您只返回一个常量值?!我相信对于现实生活场景,结果可能会有所不同,并且并行线程可能会通过在返回值之前修改值来相互导致问题,返回语句位于锁定块之外。危险! @MarcGravell:我只是在思考同样的问题时偶然发现了您的帖子,即使在阅读了您的答案之后,我仍然不确定以下内容:是否存在使用外部方法可能会中断线程的任何情况- 安全逻辑。我问这个是因为我更喜欢单点返回,并且对它的线程安全性“感觉”不好。虽然,如果 IL 相同,那么我的担忧应该是没有实际意义的。 @RaheelKhan 不,没有;他们是一样的。在 IL 级别,您不能 ret.try 区域内。【参考方案2】:

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

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

T myData;
Monitor.Enter(mutex)
try

    myData= // something

finally

    Monitor.Exit(mutex);


return myData;

【讨论】:

好吧,try/finally 确实如此——然而,锁外的返回仍然需要额外的无法优化掉的局部变量——并且需要更多的代码...... 你不能从 try 块返回;它必须以“.leave”操作码结尾。所以无论哪种情况,发出的 CIL 都应该是相同的。 你是对的 - 我刚刚查看了 IL(请参阅更新后的帖子)。我学到了一些东西;-p 酷,不幸的是,我从痛苦的时间中学到了尝试在 try 块中发出 .ret 操作码并让 CLR 拒绝加载我的动态方法:-( 我能理解;我已经完成了相当多的 Reflection.Emit,但我很懒;除非我对某事非常确定,否则我会用 C# 编写有代表性的代码,然后查看 IL。但令人惊讶的是,您以 IL 术语开始思考的速度之快(即对堆栈进行排序)。【参考方案3】:

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

【讨论】:

这是正确的,其他响应者似乎缺少这一点。他们制作的简单示例可能会产生相同的 IL,但对于大多数现实生活场景而言并非如此。 我很惊讶其他答案不谈论这个 在这个示例中,他们正在讨论使用堆栈变量来存储返回值,即只有在锁之外的 return 语句,当然还有变量声明。另一个线程应该有另一个堆栈,因此不会造成任何伤害,对吗? 我不认为这是一个有效点,因为另一个线程可以更新返回调用和将返回值实际分配给主线程上的变量之间的值。无论哪种方式,返回的值都不能更改或保证与当前实际值一致。对吗? 这个答案不正确。另一个线程无法更改局部变量。局部变量存放在栈中,每个线程都有自己的栈。顺便说一句,线程堆栈的默认大小是1 MB。【参考方案4】:

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

return f(...)

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

【讨论】:

【参考方案5】:

视情况而定,

我将在这里违背常规。我通常会回到锁内。

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

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

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

对比

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

我发现案例 2 更容易阅读,更难搞砸,尤其是对于短的 sn-ps。

【讨论】:

【参考方案6】:

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

【讨论】:

【参考方案7】:

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

【讨论】:

【参考方案8】:

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

1) 进入锁

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

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

4) 退出锁

5) 退货。

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

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

【讨论】:

【参考方案9】:

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

总结和补充现有答案:

accepted answer 表明,无论您在 C# 代码中选择哪种语法形式,在 IL 代码中 - 因此在运行时 - return 直到 锁被释放之后。

即使将return 放在 lock 块内,严格来说,也歪曲了控制流[1],它在语法上是方便,因为它消除了将返回值存储在 aux 中的需要。局部变量(在块外声明,以便可以与块外的return 一起使用) - 请参阅Edward KMETT's answer。

单独 - 这方面是问题的附带,但可能仍然令人感兴趣(Ricardo Villamil's answer 试图解决它,但我认为不正确) - 将 lock 语句与return 语句 - 即,在防止并发访问的块中获取 return 的值 - 仅有意义地“保护”调用者范围内的返回值如果它没有'获得后实际上需要保护,适用于以下场景:

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

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

请注意,在这种情况下,调用方会收到值的 快照(副本)[2] - 到调用方检查时,它可能不再是原始数据结构中的当前值。

在任何其他情况下,锁定必须由调用者执行,而不是(仅)在方法内部。


[1] Theodor Zoulias 指出从技术上讲,将 return 放置在 trycatchusingifwhilefor 中也是如此, ...声明;然而,lock 声明的特定目的可能会引起对真正控制流的审查,这一点已被提出并受到了很多关注。

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

【讨论】:

关于你的答案的当前状态(修订版13),你还在猜测lock存在的原因,并从return语句的位置推断出含义。这是一个与这个问题无关的讨论恕我直言。我还发现 "misrepresents" 的用法非常令人不安。如果从lock 返回错误地表示了控制流,那么从trycatchusingifwhilefor 和任何其他位置返回也可以这样说语言的构造。这就像说 C# 充斥着控制流的错误表述。耶稣... “这就像说 C# 中充斥着控制流的错误陈述”——嗯,这在技术上是正确的,如果您选择这样理解,术语“错误陈述”只是一种价值判断。对于tryif,...我个人甚至不会考虑它,但在lock 的背景下,具体来说,这个问题出现在我身上——如果它没有出现在其他人身上同样,这个问题永远不会被问到,接受的答案也不会竭尽全力调查真实行为。

以上是关于return 语句应该在锁的内部还是外部?的主要内容,如果未能解决你的问题,请参考以下文章

重构此函数以在 if 语句 Javascript 内部和外部一致地使用“return”

怎样查询出SQLSERVER被锁的表,以锁表的SQL语句

Java try/catch/finally内部执行顺序&外部语句何种情况下执行

visA异步锁怎么使用

[原创]MySQL innodb_rollback_on_timeout参数对锁的影响

“使用”应该在命名空间内部还是外部? [复制]