-2

This is just sample test code that I'm trying to get to work. I know there are better ways to report progress but I just want to know why this code is not working. I'm reading a file and reporting progress, but it freezes the UI, and waits until the entire task is done.

private async void runtTasksButton_Click(object sender, RoutedEventArgs e)
{
    List<Task> tasks = new List<Task>();
    var t = Task.Factory.StartNew(() =>
        {
            FileStream fs = File.OpenRead(@"C:\Whatever.txt");
            var totalLength = fs.Length;

            this.Dispatcher.BeginInvoke(new Action(() =>
            {
                progressBar1.Maximum = totalLength;
            }), null);

            using (fs)
            {
                byte[] b = new byte[1024];

                while (fs.Read(b, 0, b.Length) > 0)
                {
                    this.Dispatcher.BeginInvoke(new Action(() =>
                    {
                        progressBar1.Value += 1024;
                    }), null);
                };
            }
        });
    tasks.Add(t);
    await Task.WhenAll(tasks.ToArray());
}

EDIT: I know about BackgroundWorker and async/await. But, I'm just trying to figure out this block of code in order to get a deeper understanding of the TPL. I did use the FileStream.ReadAsync() method and was able to update the ProgressBar at runtime as such:

private void runtTasksButton_Click(object sender, RoutedEventArgs e)
{
    progressTextBox.Text = "";
    label1.Content = "Milliseconds: ";
    progressBar1.Value = 0;
    progressBar2.Value = 0;

    var watch = Stopwatch.StartNew();
    List<Task> tasks = new List<Task>();
    var t1 = Task.Factory.StartNew(() => ReadFile(@"C:\BigFile1.txt", progressBar1));
    var t2 = Task.Factory.StartNew(() => ReadFile(@"C:\BigFile2.txt", progressBar2));

    tasks.Add(t1);
    tasks.Add(t2);

    Task.Factory.ContinueWhenAll(tasks.ToArray(),
        result =>
        {
            var time = watch.ElapsedMilliseconds;
            this.Dispatcher.BeginInvoke(new Action(() =>
                label1.Content += time.ToString()));
        });
}

private async Task ReadFile(string path, ProgressBar progressBar)
{
    FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None, 8192, true);
    var totalLength = fs.Length;

    await this.Dispatcher.BeginInvoke(new Action(() =>
    {
        progressBar.Maximum = totalLength;
    }));

    using (fs)
    {
        byte[] b = new byte[8192];
        while (await fs.ReadAsync(b, 0, b.Length) > 0)
        {
            await this.Dispatcher.BeginInvoke(new Action(() =>
            {
                progressBar.Value += 8192;
            }));
        };
    }
}

However, now the control goes into the ContinueWhenAll without waiting for the file reading Tasks to complete. But the Tasks do update the respective ProgressBars as intended. If someone could give some explanations, that would be appreciated.

JSteward
  • 6,833
  • 2
  • 21
  • 30
dotnetzen
  • 166
  • 1
  • 9
  • 1
    http://www.wpf-tutorial.com/misc/multi-threading-with-the-backgroundworker/ – zaitsman Dec 17 '17 at 06:54
  • Possible duplicate of [How to use WPF Background Worker](https://stackoverflow.com/questions/5483565/how-to-use-wpf-background-worker) – zaitsman Dec 17 '17 at 06:55
  • 2
    If you are using `async/await` correctly there is generally no need for `Dispatcher.BeginInvoke` because when the task completes, control is returned to the original thread, in this case the UI thread. The problem here is that you are creating another CPU-bound `Task` (`Task.Factory.StartNew`) which most likely will be a worker thread. Don't use CPU-bound tasks with I/O-bound operations such as reading a file. Use the `xxxAsync` versions of the operations in the first place. No need for `Task.Run` or equivalent –  Dec 17 '17 at 06:56
  • MickyD, please read my edited question. What is the difference between a CPU bound task and the xxxAsync versions of operations? – dotnetzen Dec 17 '17 at 07:19
  • https://stackoverflow.com/questions/868568/what-do-the-terms-cpu-bound-and-i-o-bound-mean –  Dec 17 '17 at 11:32
  • MickyD, I think that link addresses a different question. You mentioned to use the xxxAsync methods rather than CPU-bound task. But, I think they are both the same..the xxxAsync methods are just convenient wrappers around TPL Tasks. – dotnetzen Dec 18 '17 at 07:45
  • And, I don't know why this question was down-voted. It seems like a legit question with a legit answer provided by Kevin Gosse. This question was not asking about BackgroundWorker or how to update progress. – dotnetzen Dec 18 '17 at 07:46
  • SideNote: use `Progress` object for your progress bar to avoid `Invokes` - it saves the context on the moment of creation. – VMAtm Dec 19 '17 at 00:54
  • @dotnetzen incorrect –  Dec 19 '17 at 05:02

1 Answers1

1

This is a classic trap with Task.Factory.StartNew. The method returns a Task<T>, where T is the return type of your callback. Your callback is ReadFile which returns a Task. Therefore, you end up with a Task<Task>, and you're waiting on the outer task while you should be waiting on the inner task.

Two solutions:

  1. The recommended one: use Task.Run instead of Task.Factory.StartNew. That's a general recommendation, you shouldn't ever use Task.Factory.StartNew unless you know it's the right solution. Task.Run avoids many trap, such as the one you ran into.

    var t1 = Task.Run(() => ReadFile(@"C:\BigFile1.txt", progressBar1));
    
  2. If you ever have a legitimate reason to use Task.Factory.StartNew, then you can use the .Unwrap() method on the task to be able to wait on the inner one:

    var t1 = Task.Factory.StartNew(() => ReadFile(@"C:\BigFile1.txt", progressBar1)).Unwrap();
    
Kevin Gosse
  • 38,392
  • 3
  • 78
  • 94