【问题标题】:Can I make a WinForms OpenFileDialog.ShowDialog not yield the main thread?我可以让 WinForms OpenFileDialog.ShowDialog 不产生主线程吗?
【发布时间】:2016-01-09 03:10:30
【问题描述】:

我有一个 WinForms 程序,它下载然后转换一组文件,每个文件都通过一个批处理线程在各自的线程上进行,用于报告总体进度和工作完成情况。每个文件将在其自己的线程上下载,然后一旦下载,它将通过主线程的第二个批处理线程传递到自己的转换线程。通过主线程进行这种传递的原因是,一旦下载了第一个文件,主线程就可以提示用户为所有这些文件的转换版本提供保存位置。

我遇到的问题是 OpenFileDialog.ShowDialog() 方法(用于从用户获取保存目录)阻止了第一个完成的下载线程的主线程回调。这反过来又允许其他下载线程完成(不是问题),但随后也开始执行他们对主线程的回调,此时他们都点击 OpenFileDialog.ShowDialog() 否则他们不会像我的第二个批处理线程那样做在第一次转换时运行,因此它们将能够被添加到批处理线程管理器中。

我正在使用 Dispatcher.CurrentDispatcher.BeginInvoke(myCallbackDelegate, DispatcherPriority.Normal, myParameter) 以便在项目工作完成时对主线程进行回调。我在下面创建了一个简化示例来演示问题代码。

public partial class TestForm : Form
{
    private readonly Dispatcher dispatcher = Dispatcher.CurrentDispatcher;

    //An instance of my own BatchBackgroundWorkerThread<T> class; this class parrarlelizes tasks and can report their individual and overall progress, completion and cancellation.
    private readonly BatchBackgroundWorkerThread<int> conversionBatchBackgroundWorker = new BatchBackgroundWorkerThread<int>();

    public TestForm()
    {
        InitializeComponent();

        for (int threadIndex = 0; threadIndex < 4; threadIndex++)
        {
            new Thread(DoSomeBackgroundWork).Start(threadIndex);
            Thread.Sleep(10);
        }
    }

    private void DoSomeBackgroundWork(object threadIndex)
    {
        Thread.Sleep(1000);

        dispatcher.BeginInvoke(new ParameterizedThreadStart(WorkCompletedCallback), DispatcherPriority.Normal, threadIndex);
    }

    private void WorkCompletedCallback(object threadIndex)
    {
        //Waits for CanQueueNewItems to be in a valid readable state.
        conversionBatchBackgroundWorker.WaitForPendingRun();

        //CanQueueNewItems is true when the batch background worker's batch thread has been launched and its internal CountdownEvent and cancellationState have been initialized.
        if (conversionBatchBackgroundWorker.IsBusy && conversionBatchBackgroundWorker.CanQueueNewItems)
            //Queues the item in the BatchBackgroundWorker for parrarelization with any other items.
            //NOTE: This code is not currently hit unless the user makes a dialog selection before another callback can reach the above if statement.
            conversionBatchBackgroundWorker.Additem(threadIndex);
        else
        {
            FolderBrowserDialog.ShowDialog();

            conversionBatchBackgroundWorker.RunWorkerAsync(new int[] { (int)threadIndex }, FolderBrowserDialog.SelectedPath);
        }
    }
}

我已尝试将 DispatcherPriority 更改为 DispatcherPriority.SystemIdle - 这使得所有待处理的 ShowDialog 对话框在必须单击“确定”或“取消”之前出现,但结果仍然相同。

有没有办法阻止 ShowDialog 允许其他挂起的回调到主线程执行?

更新: 我已经修改了上面的代码,以更接近地反映在我的实际应用程序中发生的事情。所有回调操作都需要排入我创建的 BatchBackgroundWorker 类中,以便执行它们的相关转换。这就是为什么我不能在调用 ShowDialog() 之前简单地设置一个变量,说它已被调用,因为剩余的回调将尝试将自己排入当前尚未启动的 BatchBackgroundWorker (此时 BatchBackgroundWorker 线程未运行为用户对对话框的响应等待启动)。

【问题讨论】:

  • 从技术上讲,阻塞当前线程是使对话框成为对话框的原因,但我可以看到您对此的推理。
  • @PCLuddite 特定的对话框实现是“阻塞的”(因为同一函数中的后续代码不会执行)但进程仍然调度(处理)事件,只是在对话框拥有的循环中,这就是 UI 仍然起作用的方式。 OP遇到的问题与此有关。
  • @user2864740 没说不是。
  • 不管怎样,主要问题是:,如果事件处理被完全抑制,则(对话框本身的)UI 将无法运行。我真的很讨厌“建议”沿着消息拦截的路线走,因为这只会使问题/问题复杂化。

标签: c# multithreading winforms callback dispatcher


【解决方案1】:

只需反转语句,以便在打开对话框之前设置标志。

wasDialogShown = true;
OpenFileDialog.ShowDialog();

您还应该锁定与wasDialogShown 一起使用的代码部分:

private readonly object dialogLock = new Object();

private void WorkCompletedCallback(object threadIndex)
{
    bool wasShown;
    lock(dialogLock) {
        wasShown = wasDialogShown;
        wasDialogShown = true;
    }
    if (wasShown) {
        MessageBox.Show("Success!");
    } else {
        OpenFileDialog.ShowDialog();
    }
}

Lock Statement 可防止另一个线程在您读取 wasDialogShown 之后但在您将其设置为 true 之前读取它。


更新(响应您的更新)

解决方案可能是有 2 个状态变量:dialogLaunchedconversionWorkerLaunched。第一个返回任务启动对话框。此任务和在启动对话之后但在启动转换工作程序之前完成的任务被添加到队列中。当对话框关闭时,转换开始,所有排队的任务都添加到其中。启动转换后返回的任务会立即添加到工作线程中。

【讨论】:

  • 道歉 - 我过度简化了我的代码以试图澄清问题,但结果部分隐藏了它! wasDialogShown 实际上不是我的应用程序中的变量,因为“成功”取决于我创建并在调用 ShowDialog() 后最初启动的 BatchBackgroundWorker 类的运行。请查看我的更新答案。
  • 啊,我明白了,你的意思是这样:因为 ShowDialog() 是一个阻塞操作,所以让所有后续回调也阻塞,直到它们检测到 conversionBatchBackgroundWorker.CanQueueNewItems 为真?如果我的理解是正确的,那么阻止它们的最佳方法是什么?首先想到的是:while (!conversionBatchBackgroundWorker.CanQueueNewItems) { } - 但我知道这可能不是最佳实践......
  • 我尝试了while (dialogLaunched) { } 轮询循环,或者ManualResetEvent.WaitOne() 方法。不幸的是,因为这一切都发生在主线程上,方法#2失败,因为它使整个线程进入睡眠状态,方法#1失败,因为它无限循环并且显然不允许我假设是Windows消息循环重新分配时间实际向用户显示第一个对话框!我认为进入无限循环的后续回调需要在某种“pendingForConversionQueue”中排队。我会报告我的发现。
  • 另一个想法是将协调也委托给后台线程。然后这个将启动工作线程。 UI 线程(主线程)只会启动协调线程。这样,对话就不会再阻碍协调了。
猜你喜欢
  • 2012-02-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-07
  • 1970-01-01
  • 1970-01-01
  • 2015-02-24
  • 2017-08-24
相关资源
最近更新 更多