BackgroundWorker OnWorkCompleted 抛出跨线程异常

发布于 2024-07-19 08:41:10 字数 2022 浏览 4 评论 0原文

我有一个用于数据库分页的简单 UserControl,它使用控制器来执行实际的 DAL 调用。 我使用 BackgroundWorker 来执行繁重的工作,并在 OnWorkCompleted 事件中重新启用一些按钮,更改 TextBox.Text 属性并为父表单引发一个事件。

表单 A 保存我的用户控件。 当我单击某个打开表单 B 的按钮时,即使我没有在“那里”执行任何操作并只是将其关闭,并尝试从数据库中引入下一页,OnWorkCompleted 也会被调用在工作线程(而不是我的主线程)上,并抛出跨线程异常。

目前,我在处理程序中添加了对 InvokeRequired 的检查,但是 OnWorkCompleted 的全部目的不是要在主线程上调用吗? 为什么它不能按预期工作?

编辑:

我已成功将问题范围缩小到 arcgis 和 BackgroundWorker。 我有以下解决方案,向 arcmap 添加一个命令,打开一个带有两个按钮的简单 Form1

第一个按钮运行一个 BackgroundWorker,它会休眠 500 毫秒并更新计数器。 在 RunWorkerCompleted 方法中,它检查 InvokeRequired,并更新标题以显示该方法最初是在主线程还是工作线程内运行。 第二个按钮仅打开 Form2,其中不包含任何内容。

首先,对 RunWorkerCompletedare 的所有调用都是在主线程内进行的(正如预期的那样 - 这就是 RunWorkerComplete 方法的全部要点,至少根据我从 MSDN on BackgroundWorker)

打开后并关闭 Form2RunWorkerCompleted 始终在工作线程上调用。 我想补充一点,我可以按原样保留此解决方案(检查 RunWorkerCompleted 方法中的 InvokeRequired ),但我想了解为什么会发生这种情况我的期望。 在我的“真实”代码中,我希望始终知道在主线程上调用了 RunWorkerCompleted 方法。

我设法在 BackgroundTesterBtn 中的 form.Show(); 命令中找到问题 - 如果我使用 ShowDialog() ,我没问题(RunWorkerCompleted 始终在主线程上运行)。 我确实需要在 ArcMap 项目中使用 Show(),这样用户就不会被绑定到表单。

我还尝试在正常的 WinForms 项目上重现该错误。 我添加了一个简单的项目,只在没有 ArcMap 的情况下打开第一个表单,但在这种情况下,我无法重现该错误 - RunWorkerCompleted 在主线程上运行,无论我是否使用 Show()ShowDialog(),在打开 Form2 之前和之后。 我尝试在 Form1 之前添加第三个表单作为主表单,但它并没有改变结果。

这里是我的简单 sln (VS2005sp1) - 它需要

ESRI.ArcGIS.ADF(9.2.4.1420)

ESRI .ArcGIS.ArcMapUI(9.2.3.1380)

ESRI.ArcGIS.SystemUI (9.2.3.1380)

I have a simple UserControl for database paging, that uses a controller to perform the actual DAL calls. I use a BackgroundWorker to perform the heavy lifting, and on the OnWorkCompleted event I re-enable some buttons, change a TextBox.Text property and raise an event for the parent form.

Form A holds my UserControl. When I click on some button that opens form B, even if I don't do anything "there" and just close it, and try to bring in the next page from my database, the OnWorkCompleted gets called on the worker thread (and not my Main thread), and throws a cross-thread exception.

At the moment I added a check for InvokeRequired at the handler there, but isn't the whole point of OnWorkCompleted is to be called on the Main thread? Why wouldn't it work as expected?

EDIT:

I have managed to narrow down the problem to arcgis and BackgroundWorker. I have the following solution wich adds a Command to arcmap, that opens a simple Form1 with two buttons.

The first button runs a BackgroundWorker that sleeps for 500ms and updates a counter.
In the RunWorkerCompleted method it checks for InvokeRequired, and updates the title to show whethever the method was originaly running inside the main thread or the worker thread.
The second button just opens Form2, which contains nothing.

At first, all the calls to RunWorkerCompletedare are made inside the main thread (As expected - thats the whold point of the RunWorkerComplete method, At least by what I understand from the MSDN on BackgroundWorker)

After opening and closing Form2, the RunWorkerCompleted is always being called on the worker thread. I want to add that I can just leave this solution to the problem as is (check for InvokeRequired in the RunWorkerCompleted method), but I want to understand why it is happening against my expectations. In my "real" code I'd like to always know that the RunWorkerCompleted method is being called on the main thread.

I managed to pin point the problem at the form.Show(); command in my BackgroundTesterBtn - if I use ShowDialog() instead, I get no problem (RunWorkerCompleted always runs on the main thread). I do need to use Show() in my ArcMap project, so that the user will not be bound to the form.

I also tried to reproduce the bug on a normal WinForms project. I added a simple project that just opens the first form without ArcMap, but in that case I couldn't reproduce the bug - the RunWorkerCompleted ran on the main thread, whether I used Show() or ShowDialog(), before and after opening Form2. I tried adding a third form to act as a main form before my Form1, but it didn't change the outcome.

Here is my simple sln (VS2005sp1) - it requires

ESRI.ArcGIS.ADF(9.2.4.1420)

ESRI.ArcGIS.ArcMapUI(9.2.3.1380)

ESRI.ArcGIS.SystemUI (9.2.3.1380)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

土豪我们做朋友吧 2024-07-26 08:41:11

OnWorkCompleted 的全部意义不是要在主线程上调用吗? 为什么它不能按预期工作?

不,不是。
你不能只是在任何旧线程上运行任何旧东西。 线程不是你可以简单地说“请运行这个”的礼貌对象。

一个更好的线程心智模型是货运列车。 一旦开始,它就走上了自己的轨道。 你无法改变它的进程或阻止它。 如果你想影响它,你要么必须等到它到达下一个火车站(例如:让它手动检查某些事件),要么让它脱轨(Thread.Abort和CrossThread异常有与火车脱轨的后果几乎相同......小心!)。

Winforms 控件在某种程度上支持这种行为(它们有 Control.BeginInvoke ,它允许您在 UI 线程上运行任何函数),但这只能起作用,因为它们有一个特殊的钩子Windows UI 消息泵并编写一些特殊的处理程序。 按照上面的类比,他们的火车在车站报到并定期寻找新的方向,您可以使用该设施发布您自己的方向。

BackgroundWorker 被设计为通用目的(它不能绑定到 Windows GUI),因此它不能使用 Windows Control.BeginInvoke 功能。 它必须假设您的主线程是一个不可阻挡的“火车”,在做自己的事情,因此完成的事件必须在工作线程中运行,或者根本不运行。

但是,当您使用 winforms 时,在 OnWorkCompleted 处理程序中,您可以使用我提到的 BeginInvoke 功能让窗口执行另一个回调多于。 像这样:

// Assume we're running in a windows forms button click so we have access to the 
// form object in the "this" variable.
void OnButton_Click(object sender, EventArgs e )
    var b = new BackgroundWorker();
    b.DoWork += ... blah blah

    // attach an anonymous function to the completed event.
    // when this function fires in the worker thread, it will ask the form (this)
    // to execute the WorkCompleteCallback on the UI thread.
    // when the form has some spare time, it will run your function, and 
    // you can do all the stuff that you want
    b.RunWorkerCompleted += (s, e) { this.BeginInvoke(WorkCompleteCallback); }
    b.RunWorkerAsync(); // GO!
}

void WorkCompleteCallback()
{
    Button.Enabled = false;
    //other stuff that only works in the UI thread
}

另外,不要忘记这一点:

您的 RunWorkerCompleted 事件处理程序应始终在访问 Result 属性之前检查 Error 和 Canceled 属性。 如果引发异常或取消操作,则访问 Result 属性会引发异常。

Isn't the whole point of OnWorkCompleted is to be called on the Main thread? Why wouldn't it work as expected?

No, it's not.
You can't just go running any old thing on any old thread. Threads are not polite objects that you can simply say "run this, please".

A better mental model of a thread is a freight train. Once it's going, it's off on it's own track. You can't change it's course or stop it. If you want to influence it, you either have to wait til it gets to the next train station (eg: have it manually check for some events), or derail it (Thread.Abort and CrossThread exceptions have much the same consequences as derailing a train... beware!).

Winforms controls sort of support this behaviour (They have Control.BeginInvoke which lets you run any function on the UI thread), but that only works because they have a special hook into the windows UI message pump and write some special handlers. To go with the above analogy, their train checks in at the station and looks for new directions periodically, and you can use that facility to post it your own directions.

The BackgroundWorker is designed to be general purpose (it can't be tied to the windows GUI) so it can't use the windows Control.BeginInvoke features. It has to assume that your main thread is an unstoppable 'train' doing it's own thing, so the completed event has to run in the worker thread or not at all.

However, as you're using winforms, in your OnWorkCompleted handler, you can get the Window to execute another callback using the BeginInvoke functionality I mentioned above. Like this:

// Assume we're running in a windows forms button click so we have access to the 
// form object in the "this" variable.
void OnButton_Click(object sender, EventArgs e )
    var b = new BackgroundWorker();
    b.DoWork += ... blah blah

    // attach an anonymous function to the completed event.
    // when this function fires in the worker thread, it will ask the form (this)
    // to execute the WorkCompleteCallback on the UI thread.
    // when the form has some spare time, it will run your function, and 
    // you can do all the stuff that you want
    b.RunWorkerCompleted += (s, e) { this.BeginInvoke(WorkCompleteCallback); }
    b.RunWorkerAsync(); // GO!
}

void WorkCompleteCallback()
{
    Button.Enabled = false;
    //other stuff that only works in the UI thread
}

Also, don't forget this:

Your RunWorkerCompleted event handler should always check the Error and Cancelled properties before accessing the Result property. If an exception was raised or if the operation was canceled, accessing the Result property raises an exception.

凉风有信 2024-07-26 08:41:11

它看起来像一个错误:

http://connect.microsoft.com/ VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=116930

http ://thedatafarm.com/devlifeblog/archive/2005/12/21/39532.aspx

所以我建议使用防弹(伪代码):

if(control.InvokeRequired)
  control.Invoke(Action);
else
  Action()

It looks like a bug:

http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=116930

http://thedatafarm.com/devlifeblog/archive/2005/12/21/39532.aspx

So I suggest using the bullet-proof (pseudocode):

if(control.InvokeRequired)
  control.Invoke(Action);
else
  Action()
对风讲故事 2024-07-26 08:41:11

BackgroundWorker 检查委托实例是否指向支持接口ISynchronizeInvoke 的类。 您的 DAL 层可能没有实现该接口。 通常,您会在 Form 上使用 BackgroundWorker,它确实支持该接口。

如果您想使用 DAL 层中的 BackgroundWorker 并希望从那里更新 UI,您有三个选择:

  • 您可以继续调用 Invoke 方法
  • 来实现DAL 类上的接口 ISynchronizeInvoke ,并在调用 BackgroundWorker 之前手动重定向调用(只有三个方法和一个属性)
  • (因此,在 UI 线程上),以调用SynchronizationContext.Current 并将内容实例保存在实例变量中。 然后,SynchronizationContext 将为您提供 Send 方法,该方法将完全执行 Invoke 的操作。

The BackgroundWorker checks whether the delegate instance, points to a class which supports the interface ISynchronizeInvoke. Your DAL layer probably does not implement that interface. Normally, you would use the BackgroundWorker on a Form, which does support that interface.

In case you want to use the BackgroundWorker from the DAL layer and want to update the UI from there, you have three options:

  • you'd stay calling the Invoke method
  • implement the interface ISynchronizeInvoke on the DAL class, and redirect the calls manually (it's only three methods and a property)
  • before invoking the BackgroundWorker (so, on the UI thread), to call SynchronizationContext.Current and to save the content instance in an instance variable. The SynchronizationContext will then give you the Send method, which will exactly do what Invoke does.
耳根太软 2024-07-26 08:41:11

避免 GUI 中跨线程问题的最佳方法是 使用 SynchronizationContext

The best approach to avoid issues with cross-threading in GUI is to use SynchronizationContext.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文