我们有一个Winforms应用程序,它启动一个WPF对话框,我将其称为向导。向导的目的是打开许多文本文件并将其内容保存到数据库中。将这些文件保存到DB所需的时间从15秒到60秒不等。为了提供向导UI响应,将文本文件保存到数据库的过程是在BackgroundWorker线程上完成的。不幸的是,由于Winforms主机应用程序中的一些遗留代码,向导在80-90%的时间内完全没有响应。因此,向导在其自己的工作线程上启动。
//Put the Wizard on a separate thread to maintain UI //responsiveness during the export process Thread t = new Thread(LaunchBatchExportWizardView); t.SetApartmentState(ApartmentState.STA); t.Name = "WizardThread"; t.Start();
总而言之,我们有三个主线程,主线程支持Winforms主机,Wizard线程和BackgroundWorker线程。用户可以通过单击向导上的按钮来暂停此导出过程,该按钮向BackgroundWorker线程发送CancelAsync消息。在BackgroundWorker_DoWork事件处理程序中,我们检查此取消消息,如果找到,则停止处理并返回。这将触发BackgroundWorker_RunWorkerCompleted事件处理程序,我可以使用Thread.CurrentThread.Name检查已返回控件的线程的标识。
用户第一次暂停BackgroundWorker进程时,控件将返回到向导线程。如果用户想要恢复导出过程,我们调用RunWorkerAsync,然后启动BackgroundWorker_DoWork事件处理程序。从观察中我可以看到这个处理程序不使用它以前使用的相同线程,而是使用线程池中的新线程。出于调试目的,我给这个线程命名。
if(Thread.CurrentThread.Name == null) { Thread.CurrentThread.Name = "MyBackgroundWorkerThread" + "_" + _threadCounter.ToString(); _threadCounter++; }
第一个BackgroundWorker线程名为MyBackgroundWorkerThread_1,第二个名为MyBackgroundWorkerThread_2,等等。
稍后当用户决定再次暂停该过程时,控件不会返回到BackgroundWorker_RunWorkerCompleted事件处理程序中的向导线程(因为这是第一次),而是返回到某个新线程。但是,这不是致命的,用户仍然可以恢复,导出过程将从中断处继续。
但是,如果我们尝试在与数据库的连接丢失的情况下启动自定义警告对话框,则会出现问题。显然,如果向导无法与数据库通信,则无法执行其主要任务。如果我们在一个或多个暂停/恢复周期后启动此对话框,则会抛出异常,并显示消息“调用线程必须是STA,因为许多UI元素需要这样做”。由于此要求,向导线程显式设置为STA(请参阅上面的相关代码),因此如果在任何暂停/恢复周期之前发生数据库连接丢失,一切正常。这个新线程,显然不是STA,因此抛出异常。
我尝试过的一个选项是测试是否在我们想要启动DatabaseConnectivityLoss对话框的时候,我们在Wizard线程上。由于Wizard线程是一个显式命名的线程(参见上面的代码),我只是测试当前线程的name属性是否为null:
if (Thread.CurrentThread.Name == null) { if (Application.Current == null) { new Application(); } Thread t = new Thread(LaunchDatabaseConnectivityLossDialog); t.SetApartmentState(ApartmentState.STA); t.Name = "NewStaThread"; t.Start(); }
这可以在没有前面提到的异常的情况下启动对话框,但是当我们在恢复连接后尝试恢复导出过程时,应用程序会挂起。
我尝试的第二件事是设置SynchronizationContext变量来保存对Wizard线程的引用。我的理解是,我可以使用此引用在Wizard线程上启动DatabaseConnectivityLoss对话框,而不管当前哪个线程是当前的。为此,我在向导的构造函数中设置此变量:
_synchronizationContext = SynchronizationContext.Current; if (_synchronizationContext == null) {//always true in my case because the wizard is a child of a Winforms app _synchronizationContext = new SynchronizationContext(); }
但是,当我后来尝试使用此SynchronizationContext强制我的代码重新启动到向导线程时,它失败了:
`_synchronizationContext.Send(Test, null); private void Test(object placeholder) { Debug.WriteLine(Thread.CurrentThread.Name); }`
Thread.CurrentThread.Name通常返回null,在其他情况下返回“NewStaThread”。我不想暗示这种行为是间歇性的。只是我尝试了很多不同的变化,并且在不同情况下从许多不同的位置调用了这种方法。
我的印象是,synchronizationContext应该包含对向导线程的引用,当我调用Send方法时,回调方法应该在向导线程上执行。
任何人都可以看到我的哪些假设无效,或者建议解决方案的途径。
从概念上讲,我相信我必须强制我的应用程序从DoWork处理程序返回到向导线程,或者在启动DatabaseConnectivityLoss对话框之前强制它重新启动到向导线程。据我所知,我无法访问匿名线程,直到将它设置为STA为时已晚,必须在启动之前完成。