Task.WaitAll freezes app C#

2019-02-20 19:27发布

问题:

I wanted to try out Threading.Task (C#) to run some work in parallel. In this simple example I have a form with progress bar and button. On click the RunParallel function is called. Without Task.WaitAll() it seems to run through fine. However, with the WaitAll statement the form shows and nothing happens. I dont understand what I am doing wrong in the setup below. Thanks in advance.

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:

You are waiting on the UI thread. This freezes the UI. Don't do that.

In addition, you are deadlocking because Invoke waits for the UI thread to unblock.

My advice: use async/await if at all possible.

And please do not swallow exceptions. You're creating future work for yourself that way.



回答2:

That's what WaitAll does, it blocks until all tasks have finished. The tasks can't finish because Invoke will run its action on the UI thread, which is already blocked by WaitAll

To make your code really run asynchronously, try something like this:

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++;
}

In this case, UpdateBar is called on the UI context each time a task finishes without causing any blocking.

Note, this is not production code, just a way to show how you can run a method asynchronously and update the UI without blocking. You do need to read about Tasks to understand what they do and how they work.

Using async/await in .NET 4.5+ you can write this in a much simpler way. The following will execute DoWork in the background without blocking, but still update the UI each time a Task finishes.

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 tells the compiler to generate code to execute anything below it ont the original (UI) threadwhen the task to its right finishes:



回答3:

In doWork you call this.Invoke(...) which waits for UI thread to process messages. Unfortunatelly you UI thread is not processing messages because it is waiting for all doWork(...) to finish.

Easiest fix is to change this.Invoke to this.BeginInvoke (it will send messages but no wait for them to be processed) in doWork. Although, I have to admin it is still not by-the-book as UI should not wait for anything. Simple pattern (pre async/await era):

Task.Factory.StartNew(() => {
    ... work ...
})
.ContinueWith((t) => {
    ... updating UI (if needed) ...
}, TaskScheduler.FromCurrentSynchronizationContext());


回答4:

RunParallel blocks until all the tasks are completed. Use a different mechanism to notify the UI.