void example()
{
lock (mutex)
{
//...
}
return myData;
}
void example()
{
lock (mutex)
{
//...
return myData;
}
}
我应该使用哪一个?
void example()
{
lock (mutex)
{
//...
}
return myData;
}
void example()
{
lock (mutex)
{
//...
return myData;
}
}
我应该使用哪一个?
基本上,让代码变得更加简单的方式就是最好的方式。单一的出口点是一个很好的理想,但我不会为了达到这个目标而弯曲代码... 如果替代方案是声明一个局部变量(在锁之外),初始化它(在锁内)然后返回它(在锁之外),那么我认为在锁内简单地使用“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
来保持简单,但我对任何一个都不会感到兴奋。
.try
区域内使用 ret
。 - Marc GravellProgram
、ReturnInside
和 ReturnOutside
声明为静态时才会出现问题,否则 i
就是一个局部变量,是安全的。 - ViRuSTriNiTy这并没有什么区别;它们都被编译器翻译成相同的内容。
为了澄清,两者都被有效地翻译成具有以下语义的内容:
T myData;
Monitor.Enter(mutex)
try
{
myData= // something
}
finally
{
Monitor.Exit(mutex);
}
return myData;
我肯定会把返回语句放在锁里面。否则,您将冒着另一个线程进入锁并在返回语句之前修改变量的风险,从而使原始调用方收到与预期不同的值。
如果你认为外面的锁看起来更好,但是如果你最终要更改代码,请小心:
return f(...)
这要看情况而定。
我会与众不同地建议在锁内返回值。
通常,变量mydata是一个局部变量。我喜欢在初始化变量时声明局部变量。很少有数据可以在锁外初始化我的返回值。
因此,你的比较实际上是有缺陷的。虽然理想情况下,两个选项之间的区别应该像你所写的那样,看起来倾向于第一种情况,但实际上情况要复杂得多。
void example() {
int myData;
lock (foo) {
myData = ...;
}
return myData
}
vs.
void example() {
lock (foo) {
return ...;
}
}
我认为对于短代码片段来说,第二种情况更易于阅读且更难出错。
值得一提的是,MSDN文档中有一个从锁内返回的示例。从其他答案来看,它似乎是非常类似IL的语法,但对我来说,在锁内返回似乎更加安全,因为这样可以避免另一个线程覆盖返回变量。
lock() return <expression>
语句总是:
1)进入锁定状态
2)为指定类型的值创建本地(线程安全)存储,
3)用<expression>
返回的值填充存储器,
4)退出锁定状态
5)返回存储器。
这意味着从lock语句返回的值总是在返回之前“处理”过的。
不要担心lock() return
,不要听任何人的建议))
注意:我相信这个答案在事实上是正确的,我希望它也能有所帮助,但我非常乐意根据具体反馈进行改进。
总结和补充现有答案:
接受的答案表明,无论您在C#代码中选择哪种语法形式,在IL代码中(因此在运行时),return
都不会发生,直到释放锁定时才会发生。
尽管将return
放置在lock
块内因此严格来说误导了控制流程[1],但从语法上讲,在块内使用return
可以方便地避免需要在块外声明一个辅助局部变量(以便可以在块外使用return
)以存储返回值-请参见Edward KMETT的答案。
另外-这个方面与问题无关,但可能仍然很有趣(Ricardo Villamil的答案试图解决它,但我认为是错误的)-结合lock
语句和return
语句-即,在受并发访问保护的块中获得值以return
-只有在实际上不需要保护一旦获得就会"保护"调用者范围内的返回值的情况下才有意义。这适用于以下情况:
如果返回的值是来自仅需要在添加和删除元素方面进行保护而不需要在元素本身的修改方面进行保护的集合中的元素...
...如果返回的值是值类型或字符串的实例。
请注意,在这种情况下,调用者接收到该值的快照(拷贝)[2] - 在调用者检查该值时,它可能不再是原始数据结构中的当前值。
在任何其他情况下,锁定必须由调用者执行,而不是(仅)在方法内执行。
[1] Theodor Zoulias 指出,在 try
、catch
、using
、if
、while
、for
等语句中放置 return
也是技术上正确的;然而,lock
语句的特定目的很可能会引起对真实控制流的审查,正如这个问题已经被问到并得到了很多关注。
[2] 访问值类型实例不可避免地创建一个线程本地的、在堆栈上的副本;尽管字符串在技术上是引用类型实例,但它们实际上表现得像值类型实例。
lock
存在的原因,并从返回语句的位置中得出含义。这是与此问题无关的讨论,在我看来。此外,我发现使用“误导”这个词相当令人不安。如果从lock
返回会误导控制流,则同样可以说从try
、catch
、using
、if
、while
、for
和语言的任何其他结构返回也是如此。这就像说C#充满了控制流误导一样。天哪... - Theodor Zouliaslock
的上下文中,这个问题引起了我的关注 - 如果其他人没有遇到同样的问题,这个问题就不会被提出,并且被接受的答案也不会费尽心思地调查真正的行为。 - mklement0