从我编写的应用程序和我继承的应用程序中,我一直希望更好地理解在后台线程上加载数据的线程安全问题。假设我有一个简单的单窗口Windows窗体应用程序,其中包含" Load"按钮和BackgroundWorker
:
按钮的Click
处理程序调用{{1}},而工作人员loadBackgroundWorker.RunWorkerAsync()
处理程序创建并初始化DoWork
类型的对象,加载时,它以表格Document
属性存储。在工作人员LoadedDocument
处理程序中,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
我的问题是:
您对此代码有什么线程安全/内存可见性问题,或者您在后台线程上加载数据并最终使用UI线程上加载的数据的目标会有什么不同? /强>
using System;
namespace BackgroundLoadTest
{
public class Document
{
public string Property1 { get; set; }
public double Property2 { get; set; }
public int Property3 { get; set; }
}
}
属性中的锁定是否足以确保后台线程中初始化的数据对UI线程可见?锁定是否必要?我真的很想了解在后台线程上加载复杂文档这个看似非常普遍的问题,同时保持GUI的响应能力,而且我知道它很棘手。
编辑:要清楚,我最关心的是内存可见性。我想确保后台线程完成的所有数据初始化在工作完成时对GUI线程可见。我不希望更改卡在CPU缓存中,并且对其他CPU上的线程保持不可见。我不知道如何更好地陈述我的担忧,因为他们对我来说仍然很模糊。
答案 0 :(得分:3)
锁定你的getter和setter什么都不做,为变量分配引用类型是一个原子操作。
这是完全错误的。锁定会引入内存障碍,从而阻止指令重新排序,并使缓存值对其他线程可见。在不同步的情况下访问来自不同线程的字段或属性(也访问字段)并不能保证始终有效,并且不能被视为正确的代码。
您正在做的是从后台线程和UI线程访问LoadedDocument属性。正如您已在其中实现锁定,这是正确的代码并且将是线程安全的。
DoWorkEventArgs
方法中的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);
}
This tutorial是关于.NET中多线程的最全面和最易理解的资源之一,我强烈推荐。您的问题已经回复here。
编辑:澄清BackgroundWorker如何同步内容
尽管如此,我很好奇BackgroundWorker中的神奇之处在于,通过e.Result传递的数据对GUI线程完全可见。
研究reference source of Background worker,结果在线程之间如何同步并不是很明显:
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时,表示状态是暴露的 可能在线程之间共享。
因此,将此属性放在您的类之上显然会通过同步其访问权限使其成员线程安全。这太棒了吗?我认同。你应该在你的代码中使用它吗?可能不是。