.NET:如何确保线程1可以看到线程2在字段中写了什么?

.NET:如何确保线程1可以看到线程2在字段中写了什么?,.net,multithreading,synchronization,.net,Multithreading,Synchronization,环境:.NET 3.5 SP1 我有两个线程:UI线程和后台工作线程。后台工作线程定期更新共享对象中的某些字段,UI线程检查这些字段。没什么了不起的-只是进度、返回值和抛出的异常。此外,当工作线程更改这些字段时,它会在UI线程上引发一些事件(通过Control.BeginInvoke) 工作线程只写入这些字段,UI线程只读取这些字段。它们不用于任何其他通信。为了提高性能,我希望避免锁定共享对象或单个属性。共享对象中永远不会有无效状态 然而,我担心处理器缓存和编译器优化之类的事情。如何避免在UI线

环境:.NET 3.5 SP1

我有两个线程:UI线程和后台工作线程。后台工作线程定期更新共享对象中的某些字段,UI线程检查这些字段。没什么了不起的-只是进度、返回值和抛出的异常。此外,当工作线程更改这些字段时,它会在UI线程上引发一些事件(通过
Control.BeginInvoke

工作线程只写入这些字段,UI线程只读取这些字段。它们不用于任何其他通信。为了提高性能,我希望避免锁定共享对象或单个属性。共享对象中永远不会有无效状态


然而,我担心处理器缓存和编译器优化之类的事情。如何避免在UI线程的事件处理程序中看不到更新的值的情况?将
volatile
添加到所有字段就足够了吗?

使用已建立的多线程准则要好得多。在.NET 4.0中提供;如果包含对的引用,则.NET 3.5也可以使用这些

无锁代码几乎不可能正确。如果不想使用生产者/消费者队列,请使用锁


如果你坚持走痛苦的道路,那么是的,
volatile
将使任何读取该变量的线程都能获得最后的写入值。

一般来说,我建议不要使用高级同步机制,因为它们众所周知很难正确执行,但在这种情况下,可以接受调用
Thread.MemoryBarrier
。当然,这假设没有原子性要求,共享对象永远不会处于半成品状态。这实际上可能比用
volatile
喷洒所有东西都要容易

object shared;

void WorkerThread()
{
  MakeChangesToSharedObject(shared);
  Thread.MemoryBarrier(); // commit the changes to main memory
}

void UIThread()
{
  Thread.MemoryBarrier(); // ensure updates are read from main memory
  UseSharedObject(shared);
}
也许设计代码使共享对象不可变会更好。然后,您所要做的就是在一个简单而快速的原子操作中交换出共享对象引用

volatile object shared;

void WorkerThread()
{
  // The following operation is safe because the variable is volatile.
  shared = GetNewSharedObject();
}

void UIThread()
{
  // The following operation is safe because the variable is volatile.
  object value = shared.SomeProperty;
}

你没事,不用担心。需要内存屏障来刷新对内存的任何挂起写入。有一个包含任何lock语句的隐式语句。Control.Begin/Invoke()需要使用锁来保护挂起的委托列表,这样就足够了


易变的需求是一个比较困难的需求,主要是因为它的确切语义没有很好的文档记录。在x86/x64硬件上,它只是防止JIT编译器在CPU寄存器中缓存变量的值。这在您的案例中不是问题,因为委托目标指向一个方法。如果方法未内联,则不会跨方法缓存变量。无法内联代理目标。

避免使用共享对象。一般来说,这是不好的做法。只需使用事件或消息在线程之间进行通信。您能否举例说明处理器缓存或编译器优化如何导致程序失败?我不明白你在说什么。@米粉饼干-好吧,如果工作线程完成了任务并在“结果”字段中写入了一些内容,但是UI线程在接下来的5个小时内仍然会在那里看到NULL,这将不会很有成效…@Pelado-例如,如果工作线程以每毫秒1%的速度更新进度,UI线程将很难在几分之一秒内处理所有100个事件。只需更新UI进度就足够了,比如说,每秒更新4次。但我如何通过活动实现这一点?我需要使用轮询和计时器。赖斯:看到我的答案了吗?只要一个线程只是在写,另一个线程只是在读——会出什么问题呢你知道吗?如果这是不明确的,那么就不要编写无锁代码。不要吹毛求疵,但这篇文章不是针对Java的吗?是的。但类似的论点也适用于C。.NET CLR的双重检查锁定已断开。在.NET中,只要使用
volatile
关键字,双重检查锁定应该是正常的。原因是在.NET中,易失性读取产生获取围栏,而易失性写入产生释放围栏。这意味着,当您将对象实例分配给变量时,必须首先提交初始化实例所需的所有写入操作。在另一端,变量的读取必须在实例成员之前完成。因此,线程基本上无法感知处于半成品状态的实例。没有那么多字段,因此使用
volatile
喷洒是可以接受的。如果一个线程只在写,另一个线程只在读,这会有多复杂?那么您可能可以将所有内容标记为
volatile
。你是对的,一个读者和一个作者可以简化很多事情。