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