跨线程操作无效:从创建该线程的线程以外的线程访问控件
我有一个场景。 (Windows窗体,C#、. NET)
- 有一个主窗体可以承载一些用户控件。
- 用户控件执行一些繁重的数据操作,因此,如果我直接调用
UserControl_Load
方法,则UI在加载方法执行期间将无响应。 - 为了克服这个问题,我将数据加载到不同的线程上(尝试尽可能少地更改现有代码)
- 我使用了一个后台工作线程来加载数据,完成后将通知应用程序它已经完成了工作。
- 现在出现了一个真正的问题。所有UI(主窗体及其子用户控件)均在主主线程上创建。在usercontrol的LOAD方法中,我根据userControl上某些控件(如文本框)的值获取数据。
伪代码如下所示:
代码1
UserContrl1_LoadDataMethod() { if (textbox1.text == "MyName") // This gives exception { //Load data corresponding to "MyName". //Populate a globale variable List<string> which will be binded to grid at some later stage. } }
它给的例外是
Cross-thread operation not valid: Control accessed from a thread other than the thread it was created on.
为了进一步了解这一点,我进行了一些谷歌搜索,并提出了一条建议,例如使用以下代码
代码2
UserContrl1_LoadDataMethod() { if (InvokeRequired) // Line #1 { this.Invoke(new MethodInvoker(UserContrl1_LoadDataMethod)); return; } if (textbox1.text == "MyName") // Now it wont give an exception { //Load data correspondin to "MyName" //Populate a globale variable List<string> which will be binded to grid at some later stage } }
但是但是...看来我又回到了正题。再次申请
变得迟钝。如果有条件,这似乎是由于执行了第1行。加载任务再次由父线程完成,而不是我产生的第三个任务。
我不知道我是对是错。我是线程新手。
我该如何解决这个问题,如果执行第1行的代码阻塞又有什么影响?
情况是这样的:我想根据控件的值将数据加载到全局变量中。我不想从子线程更改控件的值。我永远不会从子线程执行此操作。
因此,仅访问该值,以便可以从数据库中获取相应的数据。
解决方案
.NET中的控件通常不是线程安全的。这意味着我们不应从线程所在的线程之外的其他线程访问控件。为了解决这个问题,我们需要调用控件,这是第二个示例正在尝试的操作。
但是,我们要做的只是将长时间运行的方法传递回主线程。当然,这并不是我们真正想做的。我们需要重新考虑一下,以便我们在主线程上所做的所有事情都是在此处和此处设置快速属性。
我们只想使用Invoke或者BeginInvoke来完成更改UI所需的最少工作。"重载"方法应在另一个线程上执行(例如通过BackgroundWorker),然后使用Control.Invoke / Control.BeginInvoke来更新UI。这样,UI线程将可以自由处理UI事件等。
请参阅我的线程文章中的WinForms示例,尽管该文章是在BackgroundWorker到达现场之前编写的,而且恐怕我在这方面还没有进行更新。 BackgroundWorker只是稍微简化了回调。
根据Prerak K的更新评论(已删除):
I guess I have not presented the question properly. Situation is this: I want to load data into a global variable based on the value of a control. I don't want to change the value of a control from the child thread. I'm not going to do it ever from a child thread. So only accessing the value so that corresponding data can be fetched from the database.
然后,我们想要的解决方案应如下所示:
UserContrl1_LOadDataMethod() { string name = ""; if(textbox1.InvokeRequired) { textbox1.Invoke(new MethodInvoker(delegate { name = textbox1.text; })); } if(name == "MyName") { // do whatever } }
在尝试切换回控件的线程之前,请在单独的线程中进行认真的处理。例如:
UserContrl1_LOadDataMethod() { if(textbox1.text=="MyName") //<<======Now it wont give exception** { //Load data correspondin to "MyName" //Populate a globale variable List<string> which will be //bound to grid at some later stage if(InvokeRequired) { // after we've done all the processing, this.Invoke(new MethodInvoker(delegate { // load the control with the appropriate data })); return; } } }
我们需要查看Backgroundworker示例:
http://msdn.microsoft.com/zh-CN/library/system.componentmodel.backgroundworker.aspx
特别是它如何与UI层交互。根据发帖,这似乎可以回答问题。
我在FileSystemWatcher
中遇到了这个问题,发现以下代码解决了这个问题:
fsw.SynchronizingObject = this
控件然后使用当前的表单对象来处理事件,因此将在同一线程上。
解决UI跨线程问题的最干净(也是正确的)解决方案是使用SynchronizationContext,请参阅多线程应用程序文章中的"同步UI调用",它很好地解释了该问题。