【问题标题】:Is it safe to share local variable between threads (via a callback closure)?在线程之间共享局部变量是否安全(通过回调闭包)?
【发布时间】:2011-11-09 02:21:51
【问题描述】:

我想做类似以下的事情 - 基本上我正在调用一个异步操作,它将在另一个线程中调用回调,我想等待它完成“内联”。我担心的是,跨线程(栏和事件)共享的更改变量可能由于存储在寄存器中而无法同步。如果它们是成员变量,我可以将它们标记为 volatile,但 volatile 不能用于在堆栈上创建的局部变量。我可以使用成员变量,但我认为它更干净,不会因为将其全部保留在本地而使我的课程变得混乱。

Bar bar = null;
ManualResetEvent event = new ManualResetEvent(false);

foo.AsyncOperation(new Action(()=>{    
    // This delegate will be called in another thread
    bar = ...
    event.Set();
}));

event.WaitOne(timeout);
// use bar

【问题讨论】:

  • 注意:除非你在AsyncOperationWaitOne之间做其他事情,否则你最好同步运行它
  • @Marc - 好点,但我调用的 API 本质上是异步的。它基于通过网络发送到服务器的消息。在 AsyncOperaton 中设置了一条消息,并在有回复时通知回调。我大部分时间都是异步使用它,但在这种特殊情况下,我想等待结果。我在 WaitOne 中包含一个超时,以防没有回复。
  • @Shane cool - 在这种情况下使用很好;但是,您可能会惊讶于我看到人们(立即)不恰当地执行异步/连接的次数。

标签: c# .net multithreading concurrency


【解决方案1】:

是的,它将正常工作。阅读这里

http://www.albahari.com/threading/part4.aspx

The following implicitly generate full fences:Setting and waiting on a signaling construct

在信号结构中包含ManualResetEvent

如果您想知道full fence 是什么,请在同一页面中:

完整的栅栏 最简单的一种内存屏障是完整的内存 屏障(全围栏),可防止任何类型的指令重新排序 或在围栏周围缓存。调用 Thread.MemoryBarrier 会生成一个 完整的围栏;

【讨论】:

  • 酷;如果WaitOne 充当围栏,则它是安全的。但是,我确实这样做了,MSDN 会记录这个msdn.microsoft.com/en-us/library/58195swd.aspx
  • @Marc 关于线程安全和栅栏的 msdn 文档相当缺乏,如果我没记错的话,但是 msdn.microsoft.com/en-us/library/ms686355(v=vs.85).aspx The following synchronization functions use the appropriate barriers to ensure memory ordering: Wait functionsFunctions that signal synchronization objects
  • 排序,然后;我会删除我的答案
  • @Xantos - 还没有时间深入研究这个,但我认为你的推理是有道理的。那么这里的栅栏是什么——事件构造函数,Set 和 Wait?我认为基于无锁结构的推理太棘手了,如果我在更新和读取两个线程中的 bar 时锁定,代码的意图会更清楚。事件对象应该做一个合适的锁。我猜闭包捕获机制可以确保事件在两个线程中都被视为已初始化?
  • @Shane 我会说SetWait。我不确定ManualResetEvent 的创建会隐式创建内存屏障。它更可能是一个“硬排序”案例(它总是在它可以在“代码顺序”中引用之前创建(所以如果它是在第 1 行中创建并在第 2 行中引用的,则这两行不能交换)。如果你愿意,你可以添加一个“无用的”lock 让一切更清楚。显然,如果您选择 MRE 作为锁的“目标”,两个线程都会看到它。无锁结构会导致偏头痛 :-)
【解决方案2】:

我认为您的代码会起作用 - 即使它们只是堆栈变量(ManualReseetEvent 肯定不会),闭包也会提升到堆中。

但是你为什么不把所有的东西都放在 event.WaitOne() 之后的继续里面(块是 event.Set 被调用)?我认为这应该是处理这种情况的首选方式,这样你不会遇到麻烦(你根本不需要外块的Bar,你仍然可以使用MRE来检查)。

我会考虑使用 Task 对象将其转换为操作 - 这将一次性解决所有这些问题(例如从您的 AsyncOperation 返回一个任务)。然后您可以等待 Task 的结果并使用返回的 Bar ...

class Foo
{ 
 // ...
 private Task<Bar> AsyncOperation(Task<Bar> initializeBar)
 {
   return initializeBar
          .ContinueWith(
            bar => { 
                     /* do your work with bar and return some new or same bar */ 
                     return bar;
                   });
 }
}

并像这样使用它:

var task = foo.AsyncOperation(Taks.Factory.StartNew(() => { /* create and return a bar */ }));
var theBar = task.Result; // <- this will wait for the task to finish
// use your bar

PS:闭包基本上会将它们包装成一个类对象;) PPS:如果没有您的 AsyncOperation,我很难测试此代码,但它应该可以通过我所做的错误拼写/键入来解决模语法错误

【讨论】:

  • 我不太确定 - 它是如何保证工作的?通常的数据/排序规则仅适用于单线程...
  • 显然你比我更了解这些东西 - 问题是这段代码是否被放入某种寄存器(我假设)以在另一个线程中重用/重新创建)和我认为this肯定不会发生。而且我想我在 F# 使用的异步东西中看到了非常相似的东西,老实说:我已经使用了类似的东西(使用 ManualResetEvents - 本地定义的同步线程)安静了很多,从来没有遇到麻烦。但我不是 concurency-Wizzard - 也许你可以指出问题所在而不是暗示它? (没有冒犯 - 真的很感兴趣)
  • 哦等等 - 你(当然)正在考虑 Bar - 我猜是对象......嗯 - 这可能确实会带来麻烦......
  • 如果我没记错的话,有一个 ManualResetEvent.Set 可以兼作 MemoryBarrier
  • @xanatos 如果 WaitOne() 也起到内存屏障的作用,那就可以了; Set 上的内存屏障不会影响另一个线程(我们关心的那个)上的代码
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-10-02
  • 1970-01-01
  • 2011-06-23
  • 2021-08-14
  • 2016-12-12
  • 1970-01-01
相关资源
最近更新 更多