Task.WaitAll会冻结应用程序 C#

3

我想尝试使用Threading.Task(C#)并行运行一些工作。在这个简单的例子中,我有一个带有进度条和按钮的表单。单击时将调用RunParallel函数。没有Task.WaitAll(),它似乎可以正常运行。但是,在WaitAll语句下,窗体会显示,但什么也不会发生。我不明白我在下面的设置中做错了什么。 提前感谢。

public partial class MainWindow : Form
{
    public delegate void BarDelegate();
    public MainWindow()
    {
    InitializeComponent();
    }
    private void button_Click(object sender, EventArgs e)
    {
        RunParallel();
    }
    private void RunParallel() {
        int numOfTasks = 8;
        progressBar1.Maximum = numOfTasks;
        progressBar1.Minimum = 0;
        try
        { 
            List<Task> allTasks = new List<Task>();
            for (int i = 0; i < numOfTasks; i++)
            {
                allTasks.Add(Task.Factory.StartNew(() => { doWork(i); }));
            }
            Task.WaitAll(allTasks.ToArray());
        }
        catch { }  
    }
    private void doWork(object o1)
    {
        // do work...
        // then
        this.Invoke(new BarDelegate( UpdateBar )); 
    }
    private void UpdateBar()
    {
        if (progressBar1.Value < progressBar1.Maximum) progressBar1.Value++;
    }
}

1
Task.WaitAll 是一个阻塞操作。请使用 ContinueWith 或 ContinueWhenAll 方法代替。 - Dimitri
1
这就是WaitAll的作用,它会阻塞直到所有任务都完成。任务无法完成是因为Invoke将在UI线程上运行其操作,而该线程已经被WaitAll阻塞了。 - Panagiotis Kanavos
4个回答

11

你正在等待UI线程,这会冻结UI。不要这样做。

此外,由于Invoke等待UI线程解除阻塞,所以你正在死锁。

我的建议是:尽可能使用async/await

并且请不要吞噬异常。这样做只会为你自己创造未来的工作。


谢谢你的回答。你能提供一个async/await的例子吗? - timkado

4

WaitAll的作用是阻塞直到所有任务完成。但任务无法完成,因为Invoke将在UI线程上运行其操作,而该线程已被WaitAll阻塞。

要使您的代码真正异步运行,请尝试像这样:

private void RunParallel() {
    int numOfTasks = 8;
    progressBar1.Maximum = numOfTasks;
    progressBar1.Minimum = 0;
    try
    { 
        var context=TaskScheduler.FromCurrentSynchronizationContext()
        for (int i = 0; i < numOfTasks; i++)
        {
            Task.Factory.StartNew(()=>DoWork(i))
                    .ContinueWith(()=>UpdateBar(),context);
        }

    }
    catch (Exception exc)
    { 
      MessageBox.Show(exc.ToString(),"AAAAAARGH");
    }  
}
private void DoWork(object o1)
{
    // do work...

}
private void UpdateBar()
{
    if (progressBar1.Value < progressBar1.Maximum) progressBar1.Value++;
}

在这种情况下,每次任务完成而不会导致任何阻塞时,都会在UI上下文中调用UpdateBar。
注意,这不是生产代码,只是展示了如何异步运行方法并更新UI而不阻塞的一种方式。您需要了解任务的作用和工作原理。
使用.NET 4.5+中的async/await,您可以以更简单的方式编写此代码。以下代码将在后台执行DoWork而不会阻塞,但仍会在每个任务完成时更新UI。
private async void button1_Click(object sender, EventArgs e)
{
        int numOfTasks = 8;
        progressBar1.Maximum = numOfTasks;
        progressBar1.Minimum = 0;
        try
        {
            for (int i = 0; i < numOfTasks; i++)
            {
                await Task.Run(() => DoWork(i));
                UpdateBar();
            }

        }
        catch (Exception exc)
        {
            MessageBox.Show(exc.ToString(), "AAAAAARGH");
        }
    }

await 告诉编译器,在右侧的任务完成后,生成代码以在原始(UI)线程上执行其下方的任何内容:


这是一个不错的回答,但我真的无法赞同告诉初学者要 catch(Exception),然后继续执行,就好像什么都没有发生一样。 - Dour High Arch
没有人告诉初学者“继续做好像什么都没发生”。实际上,我会说“这不是生产代码”。至于你会做什么,我认为这取决于情况:在桌面应用程序中,您可以告诉用户、记录并尝试恢复,或允许应用程序崩溃。在服务器应用程序中,也许您不返回任何结果,但记录问题并向管理员发送警报。在演示应用程序中,允许应用程序崩溃或将其显示给程序员就足以让他修复代码并重试。 - Panagiotis Kanavos
谢谢Panagiotis!C#4.5的示例非常好。但是,在for循环之后,我想等待每个任务完成。那么我该如何重新构建它呢? - timkado
你可以使用 Task.WhenAll 异步等待所有传递的任务完成,然后在 UI 线程上执行代码,例如:await Task.WhenAll(myTasks); - Panagiotis Kanavos

1
在 doWork 方法中,你调用了 this.Invoke(...),它会等待 UI 线程处理消息。不幸的是,你的 UI 线程没有处理消息,因为它正在等待所有的 doWork(...) 完成。
最简单的解决方法是在 doWork 中将 this.Invoke 更改为 this.BeginInvoke(它会发送消息但不会等待消息被处理)。尽管如此,我必须承认,这仍然不符合规范,因为 UI 不应该等待任何东西。
简单模式(异步/等待之前):
Task.Factory.StartNew(() => {
    ... work ...
})
.ContinueWith((t) => {
    ... updating UI (if needed) ...
}, TaskScheduler.FromCurrentSynchronizationContext());

0

RunParallel 会阻塞直到所有任务完成。请使用其他机制来通知用户界面。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接