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


142

我只是意识到在代码的某些地方,return语句位于锁内部,有时位于外部。哪一个是最好的?

1)

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

2)

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

}

我应该使用哪一个?


如何发射Reflector并进行一些IL比较;-)。
Pop Catalin's

6
@Pop:完成-在IL方面也没有更好的选择-仅适用C#风格
Marc Gravell

1
非常有趣,哇,我今天学到了东西!
Pokus

Answers:


192

本质上,无论哪种方法都使代码更简单。单点退出是一个很好的理想选择,但是我不会为了实现它而使代码变形……并且如果替代方法是声明局部变量(在锁外),对其进行初始化(在锁内)并然后将其返回(在锁之外),那么我想说,锁内的简单“ 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为简化起见,我更喜欢,但我都不会对此感到兴奋。


15
我使用(免费且出色的)Red Gate的.NET Reflector(以前是:Lutz Roeder的.NET Reflector),但是ILDASM也会这样做。
马克·格拉韦尔

1
Reflector最强大的方面之一就是您实际上可以将IL分解为您喜欢的语言(C#,VB,Delphi,MC ++,Chrome等)
Marc Gravell

3
对于您的简单示例,IL保持不变,但这可能是因为您仅返回了恒定值?!我相信在现实生活中,结果可能会有所不同,并且并行线程可能会通过在返回值之前将其修改,而return语句不在锁块之外,从而相互造成问题。危险的!
Torbjørn

@MarcGravell:我只是在想着您的帖子时碰到了您的帖子,即使在阅读了您的答案之后,我仍然不确定以下内容:在任何情况下,使用外部方法可能会破坏线程安全逻辑。我问这个问题是因为我更喜欢单点返回并且对线程安全性不满意。虽然,如果IL相同,则无论如何我都应该关注。
Raheel Khan 2012年

1
@RaheelKhan不,没有;他们是一样的。在IL级别,您不能 ret.try区域内。
马克·格雷夫

42

没什么区别;它们都被编译器翻译成同一件事。

为了澄清起见,其中任何一个都有效地转换为具有以下语义的内容:

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

return myData;

1
好吧,尝试/最终都是如此-但是,锁外的返回仍然需要无法优化的额外本地
变量

3
您不能从try块返回;它必须以“ .leave”操作码结尾。因此,两种情况下发出的CIL应该相同。
格雷格·比奇

3
您是对的-我刚刚查看了IL(请参阅更新的帖子)。我学到了一些东西;-p
马克·

2
很酷,很不幸,我从痛苦的时刻中学到了尝试在try块中发出.ret操作码,并让CLR拒绝加载我的动态方法的方法:-(
Greg Beech

我可以联系;我做了很多的Reflection.Emit,但是我很懒。除非我非常确定,否则我将使用C#编写代表代码,然后查看IL。但是令人惊讶的是,您以多快的速度开始思考IL术语(即对堆栈进行排序)。
马克·格拉韦尔

28

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


4
这是正确的,其他响应者似乎都没有提到这一点。他们制作的简单样本可能会产生相同的IL,但对于大多数实际场景而言并非如此。
Torbjørn

4
我很惊讶其他答案没有谈到这一点
Akshat Agarwal

5
在此示例中,他们正在谈论使用堆栈变量来存储返回值,即仅在锁之外的return语句,当然还有变量声明。另一个线程应该具有另一个堆栈,因此不会造成任何伤害,对吗?
Guillermo Ruffino

3
我认为这不是正确的观点,因为另一个线程可以在返回调用与返回值实际分配给主线程上的变量之间更新值。无论哪种方式,返回的值都不能更改,或者不能保证与当前实际值一致。对?
乌罗什Joksimović

这个答案是不正确的。另一个线程无法更改局部变量。局部变量存储在堆栈中,每个线程都有自己的堆栈。顺便说一句,线程堆栈的默认大小为1 MB
Theodor Zoulias

5

这取决于,

我要反对这里的粮食。我通常会回到锁内。

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

因此,您的比较实际上是有缺陷的。理想情况下,这两个选项之间的差异将与您所写的相同,这似乎使情况1得到了点头,实际上它有点难看。

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

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

我发现案例2相当容易阅读,而且很难弄乱,特别是对于短片段。


4

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

return f(...)

如果需要在持有锁的情况下调用f(),则显然它必须位于锁内,因为为了保持一致性,在锁内保持返回是有意义的。


1

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



0

lock() return <expression> 始终声明:

1)输入锁

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

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

4)出口锁

5)退货。

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

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


-2

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

总结和补充现有的答案:

  • 接受的答案显示,不论其语法形式的你在你的选择,C#代码,在IL代码-因此在运行时-在return没有发生,直到锁被释放。

    • 因此,严格来说,即使将其return 放置lock块中也不能正确表示控制流[1],但从语法上讲,它的方便之处在于它无需将返回值存储在aux中。局部变量(在块外部声明,以便可以return在块外部使用)-请参见Edward KMETT的答案
  • 另外-这是问题的附带内容,但可能仍然很有趣(里卡多·比利亚米尔Ricardo Villamil)的答案试图解决这个问题,但我认为这是错误的)-将一个lock语句与一个return语句组合在一起-即return在一个受保护的块中获取价值并发访问-仅在获得返回值后实际上不需要保护时才有意义地“保护” 调用方范围内的返回值,这适用于以下情况:

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

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

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


[1] 西奥多Zoulias指出,在技术上也是如此放置returntrycatchusingifwhilefor,...报表; 但是,lock陈述的具体目的很可能会引起对真正控制流程的审查,这一问题已被问到并引起了广泛关注,这证明了这一点。

[2]访问值类型实例总是会创建它在线程本地的堆栈上的副本;即使字符串从技术上来说是引用类型的实例,它们也有效地表现类似值的类型的实例。


关于答案的当前状态(修订版13),您仍在推测存在的原因lock,并从return语句的位置得出含义。这是与这个问题恕我直言无关的讨论。另外,我发现“不实陈述”的使用也令人不安。如果从返回lock歪曲控制流,那么同样可以说从一回trycatchusingifwhilefor,和语言的任何其他结构。就像说C#充斥着控制流错误表示。耶稣...
Theodor Zoulias

“就像是在说C#充斥着控制流的错误陈述”-嗯,从技术上讲这是正确的,如果您选择采用这种方式,术语“错误陈述”仅是一种价值判断。带有try,,if...我个人甚至都不考虑这个问题,但是lock,特别是在的背景下,这个问题对我来说是个问题-如果其他人也没有想到,就永远不会问这个问题,并且接受的答案也不会花很多时间去调查真实的行为。
mklement0
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.