正确加载后台线程上的文档

从我编写的应用程序和我inheritance的应用程序开始,我一直希望更好地理解在后台线程上加载数据的线程安全问题。 假设我有一个简单的单窗口Windows窗体应用程序,带有“加载”按钮和BackgroundWorker

我在Visual Studio Designer中的应用程序

按钮的Click处理程序调用loadBackgroundWorker.RunWorkerAsync() ,并且worker的DoWork处理程序创建并初始化Document类型的对象,该对象在加载后存储在表单的LoadedDocument属性中。 在worker的RunWorkerCompleted处理程序中, MessageBox显示LoadedDocument的属性。 我知道这很难想象,所以我要包含完整的代码。 很抱歉,这个问题需要很长时间才能阅读。

这是表单的代码:

 using System; using System.ComponentModel; using System.Windows.Forms; namespace BackgroundLoadTest { public partial class Form1 : Form { private Document _loadedDocument; public Document LoadedDocument { get { lock (this) { return _loadedDocument; } } set { lock (this) { _loadedDocument = value; } } } public Form1() { InitializeComponent(); loadBackgroundWorker.DoWork += new DoWorkEventHandler(loadBackgroundWorker_DoWork); loadBackgroundWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(loadBackgroundWorker_RunWorkerCompleted); } void loadBackgroundWorker_DoWork(object sender, DoWorkEventArgs e) { Document d = new Document(); d.Property1 = "Testing"; d.Property2 = 1; d.Property3 = 2; this.LoadedDocument = d; } void loadBackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { MessageBox.Show("Document loaded with Property1 = " + LoadedDocument.Property1 + ", Property2 = " + LoadedDocument.Property2 + ", Property3 = " + LoadedDocument.Property3); } private void loadButton_Click(object sender, EventArgs e) { loadBackgroundWorker.RunWorkerAsync(); } } } 

这是Document类的代码:

 using System; namespace BackgroundLoadTest { public class Document { public string Property1 { get; set; } public double Property2 { get; set; } public int Property3 { get; set; } } } 

我的问题是:

您在此代码中看到了哪些线程安全/内存可见性问题,或者您在后台线程上加载数据并最终使用UI线程上加载的数据的目标会有什么不同?

LoadedDocument属性中的锁定是否足以确保在后台线程中初始化的数据对UI线程可见? 锁定是否必要? 我真的很想了解在后台线程上加载复杂文档同时保持GUI响应的看似非常普遍的问题,我知道这很棘手。

编辑:要清楚,我最关心的是内存可见性。 我想确保后台线程完成的所有数据初始化在工作完成时对GUI线程可见。 我不希望更改卡在CPU缓存中,并且对其他CPU上的线程保持不可见。 我不知道如何更好地表达我的担忧,因为他们对我来说仍然很模糊。

锁定getter和setter不会做任何事情,为变量分配引用类型是一个primefaces操作。

这是完全错误的。 锁定会引入内存屏障,从而阻止指令重新排序,并使缓存值对其他线程可见。 在不同步的情况下从不同线程访问字段或属性(也访问字段)并不能保证始终有效,也不能被认为是正确的代码

您正在做的是从后台线程和UI线程访问LoadedDocument属性。 正如您已在其中实现锁定,这是正确的代码并且将是线程安全的。

loadBackgroundWorker_DoWork方法中的loadBackgroundWorker_DoWork参数具有Result属性,该属性应该用于设置后台工作的结果。 然后可以使用RunWorkerCompletedEventArgs.Result属性来访问此值。 请尝试以下方法:

  void loadBackgroundWorker_DoWork(object sender, DoWorkEventArgs e) { Document d = new Document(); d.Property1 = "Testing"; d.Property2 = 1; d.Property3 = 2; e.Result = d; } void loadBackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { this.LoadedDocument = (Document)e.Result; MessageBox.Show("Document loaded with Property1 = " + LoadedDocument.Property1 + ", Property2 = " + LoadedDocument.Property2 + ", Property3 = " + LoadedDocument.Property3); } 

本教程是关于.NET中multithreading的最全面和最易理解的资源之一,我强烈推荐。 你的问题在这里已得到解答。


编辑:澄清BackgroundWorker如何同步内容

尽管如此,我很好奇BackgroundWorker中的魔术是什么使得通过e.Result传递的数据对GUI线程完全可见。

查看后台工作程序的参考源,如何在线程之间同步结果并不是很明显:

  private void WorkerThreadStart(object argument) { object workerResult = null; Exception error = null; bool cancelled = false; try { DoWorkEventArgs doWorkArgs = new DoWorkEventArgs(argument); OnDoWork(doWorkArgs); if (doWorkArgs.Cancel) { cancelled = true; } else { workerResult = doWorkArgs.Result; } } catch (Exception exception) { error = exception; } RunWorkerCompletedEventArgs e = new RunWorkerCompletedEventArgs(workerResult, error, cancelled); asyncOperation.PostOperationCompleted(operationCompleted, e); } 

这发生在后台线程上。 最后一行然后编组回到UI线程。 进一步查看堆栈,没有锁定语句或其他同步指令。 那么这个线程如何安全?

查看RunWorkerCompletedEventArgs ,我们找不到同步代码。 但那里有一些奇怪的属性:

 [HostProtection(SharedState = true)] public class RunWorkerCompletedEventArgs : AsyncCompletedEventArgs 

MSDN解释说:

当SharedState为true时,它指示可以在线程之间共享的状态。

因此,将此属性放在您的类之上显然可以通过同步其访问权限使其成员线程安全。 这真棒吗? 我认同。 你应该在你的代码中使用它吗? 可能不是。