为什么Window.ShowDialog在TaskScheduler任务中没有阻塞?

时间:2014-02-13 14:08:28

标签: c# multithreading task-parallel-library

我正在使用自定义TaskScheduler来执行串行任务队列。任务应该显示一个窗口,然后阻止,直到窗口自行关闭。不幸的是,调用Window.ShowDialog()似乎没有阻止,因此任务完成,窗口永远不会显示。

如果我在调用ShowDialog之后设置断点,我可以看到表单已经打开但是在正常执行下,任务似乎很快就会结束,你无法看到它。

我的TaskScheduler实现取自上一个问题:

public sealed class StaTaskScheduler : TaskScheduler, IDisposable
{
    private readonly List<Thread> threads;
    private BlockingCollection<Task> tasks;

    public override int MaximumConcurrencyLevel
    {
        get { return threads.Count; }
    }

    public StaTaskScheduler(int concurrencyLevel)
    {
        if (concurrencyLevel < 1) throw new ArgumentOutOfRangeException("concurrencyLevel");

        this.tasks = new BlockingCollection<Task>();
        this.threads = Enumerable.Range(0, concurrencyLevel).Select(i =>
        {
            var thread = new Thread(() =>
            {
                foreach (var t in this.tasks.GetConsumingEnumerable())
                {
                    this.TryExecuteTask(t);
                }
            });
            thread.IsBackground = true;
            thread.SetApartmentState(ApartmentState.STA);
            return thread;
        }).ToList();

        this.threads.ForEach(t => t.Start());
    }

    protected override void QueueTask(Task task)
    {
        tasks.Add(task);
    }
    protected override IEnumerable<Task> GetScheduledTasks()
    {
        return tasks.ToArray();
    }
    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        return Thread.CurrentThread.GetApartmentState() == ApartmentState.STA && TryExecuteTask(task);
    }

    public void Dispose()
    {
        if (tasks != null)
        {
            tasks.CompleteAdding();

            foreach (var thread in threads) thread.Join();

            tasks.Dispose();
            tasks = null;
        }
    }
}

我的申请代码:

private StaTaskScheduler taskScheduler;

...

this.taskScheduler = new StaTaskScheduler(1);

Task.Factory.StartNew(() =>
{
    WarningWindow window = new WarningWindow(
        ProcessControl.Properties.Settings.Default.WarningHeader,
        ProcessControl.Properties.Settings.Default.WarningMessage,
        processName,
        ProcessControl.Properties.Settings.Default.WarningFooter,
        ProcessControl.Properties.Settings.Default.WarningTimeout * 1000);
    window.ShowDialog();

}, CancellationToken.None, TaskCreationOptions.None, this.taskScheduler);

3 个答案:

答案 0 :(得分:4)

没有明显的错误。除了缺少之外,您没有做任何事情来确保报告在任务中引发的异常。你编写它的方式,永远不会报告这样的异常,你只会看到代码无法运行。就像一个刚刚消失的对话框。你需要写这样的东西:

    Task.Factory.StartNew(() => {
        // Your code here
        //...
    }, CancellationToken.None, TaskCreationOptions.None, taskScheduler)
    .ContinueWith((t) => {
        MessageBox.Show(t.Exception.ToString());
    }, TaskContinuationOptions.OnlyOnFaulted);

有很好的赔率,你现在会看到报告的InvalidOperationException。使用Debug + Exceptions进一步诊断它,勾选CLR异常的Thrown复选框。

请注意,此任务计划程序不会神奇地使您的代码线程安全或适合运行另一个UI线程。它不是为此而设计的,它只应用于保持单线程COM组件的快乐。您必须遵守在另一个线程上运行UI的有时严苛的后果。换句话说,请勿触摸主线程上的UI属性。并且对话框根本不像对话框,因为它没有所有者。因此,它随机消失在另一个窗口后面,或者被用户意外关闭,因为他点击了一下,从未计入过无窗口出现的窗口。

最后但并非最不重要的是SystemEvents类造成的持久痛苦。哪个需要猜测哪个线程是UI线程,它将选择第一个STA线程。如果这是您的对话框,那么您将很难非常以后很难诊断线程问题。

不要这样做。

答案 1 :(得分:2)

显然,你正在使用Stephen Toub的StaTaskScheduler旨在运行涉及UI的任务。实质上,您正在尝试在后台线程上显示带有window.ShowDialog()的模态窗口,该窗口与主UI线程无关。我怀疑window.ShowDialog()会立即完成错误,任务也是如此。等待任务并观察错误:

try
{
    await Task.Factory.StartNew(() =>
    {
        WarningWindow window = new WarningWindow(
            ProcessControl.Properties.Settings.Default.WarningHeader,
            ProcessControl.Properties.Settings.Default.WarningMessage,
            processName,
            ProcessControl.Properties.Settings.Default.WarningFooter,
            ProcessControl.Properties.Settings.Default.WarningTimeout * 1000);
        window.ShowDialog();

    }, CancellationToken.None, TaskCreationOptions.None, this.taskScheduler);

}
catch(Exception ex)
{
    MessageBox.Show(ex.Message)
}

如果确实想要显示来自后台STA线程的窗口,则需要运行Dispatcher循环:

    var task = Task.Factory.StartNew(() =>
    {
        System.Windows.Threading.Dispatcher.InvokeAsync(() =>
        {
            WarningWindow window = new WarningWindow(
                ProcessControl.Properties.Settings.Default.WarningHeader,
                ProcessControl.Properties.Settings.Default.WarningMessage,
                processName,
                ProcessControl.Properties.Settings.Default.WarningFooter,
                ProcessControl.Properties.Settings.Default.WarningTimeout * 1000);

            window.Closed += (s, e) =>
                window.Dispatcher.InvokeShutdown();
            window.Show();
        });

        System.Windows.Threading.Dispatcher.Run();

    }, CancellationToken.None, TaskCreationOptions.None, this.taskScheduler);

但请注意,此窗口对于任何主UI线程窗口都是模态的。此外,您将阻止StaTaskScheduler循环,因此在窗口关闭并退出Dispatcher.Run()之前,其他计划任务将无法运行。

答案 2 :(得分:0)

我遇到了类似的问题,在启动时我无法通过ShowDialog阻止执行。最后,我发现类属性内部是命令行参数,用于自动使用适当的凭据登录。这节省了开发时间,每次编译时都不输入用户名和密码。一旦我删除了Dialog的行为符合预期,那么搜索可能会干扰正常执行路径的进程是值得的。