Return 语句应该位于锁的内部还是外部?

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

1)

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

2)

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


}

我该用哪个?

47373 次浏览

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

为了澄清,任何一种都可以有效地翻译成以下语义:

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


return myData;

为了方便其他开发人员阅读代码,我建议使用第一种方法。

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

return f(...)

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

本质上,这使得代码更简单。单点退出是一个不错的理想选择,但是我不会为了实现它而把代码弄弯... ... 如果另一种选择是声明一个局部变量(在锁外) ,初始化它(在锁内) ,然后返回它(在锁外) ,那么我会说,在锁内简单的“ 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,但我对这两者都不感兴趣。

看情况,

我要逆着纹路走,通常我会回到锁里面。

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

所以你的比较其实是有缺陷的。虽然理想情况下,这两个选项之间的区别应该和您所写的一样,这似乎是对情况1的肯定,但实际上它有点难看。

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

对。

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

我发现情况2更容易阅读,也更难搞砸,特别是对于简短的片段。

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

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

lock() return <expression>报表总是:

1)进入锁定状态

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

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

4)出口锁

5)归还商店。

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

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

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

总结和补充现有答复:

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

    • 即使将 return 在里面放置在 lock块中,严格地说,也会错误地表示 control [1]的流程,但它在语法上是 很方便,因为它避免了在 aux 中存储返回值的需要。Local 变量(在块外声明,这样它可以与块外的 return一起使用)-参见 Edward KMETT 的回答
  • 单独地——这个方面是问题的 意外,但可能仍然有意义(Ricardo Villamil 的回答试图解决这个问题,但我认为是错误的)——将 lock语句与 return语句相结合——例如,在一个受保护不被并发访问的块中获取 return的值——只有有意义地“保护”打电话的人作用域中返回的值 如果它一旦获得就不需要保护,它适用于以下场景:

    • 如果返回的值是集合中的一个元素,只需要根据 加入和移除元素进行保护,而不需要根据对 元素本身和/或..。

    • ... 如果返回的值是 值类型绳子的实例。

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


[1] Theodor Zoulias指出,从技术上讲,将 return放在 trycatchusingifwhilefor等声明中也是如此; 然而,lock声明的具体目的可能会引起对真正控制流程的审查,正如这个问题已经被提出并得到很多关注所证明的那样。

访问一个值类型的实例总是创建一个线程本地的、在堆栈上的副本; 即使字符串在技术上是引用类型的实例,它们的行为实际上也像值类型的实例。