Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# async/await progress reporting is not in expected order

I am experimenting with async/await and progress reporting and therefore have written an async file copy method that reports progress after every copied MB:

public async Task CopyFileAsync(string sourceFile, string destFile, CancellationToken ct, IProgress<int> progress) {

  var bufferSize = 1024*1024 ;
  byte[] bytes = new byte[bufferSize];
  using(var source = new FileStream(sourceFile, FileMode.Open, FileAccess.Read)){
    using(var dest = new FileStream(destFile, FileMode.Create, FileAccess.Write)){

      var totalBytes = source.Length;
      var copiedBytes = 0;
      var bytesRead = -1;
      while ((bytesRead = await source.ReadAsync(bytes, 0, bufferSize, ct)) > 0)
      {
        await dest.WriteAsync(bytes, 0, bytesRead, ct);
        copiedBytes += bytesRead;
        progress?.Report((int)(copiedBytes * 100 / totalBytes));
      }
    }
  }
}

In a console application a create I file with random content of 10MB and then copy it using the method above:

private void MainProgram(string[] args)
{
  Console.WriteLine("Create File...");
  var dir = Path.GetDirectoryName(typeof(MainClass).Assembly.Location);
  var file = Path.Combine(dir, "file.txt");
  var dest = Path.Combine(dir, "fileCopy.txt");

  var rnd = new Random();
  const string chars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");
  var str = new string(Enumerable
                       .Range(0, 1024*1024*10)
                       .Select(i => letters[rnd.Next(chars.Length -1)])
                       .ToArray());
  File.WriteAllText(file, str);

  var source = new CancellationTokenSource();
  var token = source.Token;

  var progress = new Progress<int>();
  progress.ProgressChanged += (sender, percent) => Console.WriteLine($"Progress: {percent}%");

  var task = CopyFileAsync(file, dest, token, progress);
  Console.WriteLine("Start Copy...");
  Console.ReadLine();
}

After the application has executed, both files are identical, so the copy process is carried out in the correct order. However, the Console output is something like:

Create File...
Start Copy...
Progress: 10%
Progress: 30%
Progress: 20%
Progress: 60%
Progress: 50%
Progress: 70%
Progress: 80%
Progress: 40%
Progress: 90%
Progress: 100%

The order differs every time I call the application. I don't understand this behaviour. If I put a Breakpoint to the event handler and check each value, they are in the correct order. Can anyone explain this to me?

I want to use this later in a GUI application with a progress bar and don't want to have it jumping back and forward all the time.

like image 550
Buchter Avatar asked Dec 02 '17 12:12

Buchter


People also ask

What C is used for?

C programming language is a machine-independent programming language that is mainly used to create many types of applications and operating systems such as Windows, and other complicated programs such as the Oracle database, Git, Python interpreter, and games and is considered a programming foundation in the process of ...

What is the full name of C?

In the real sense it has no meaning or full form. It was developed by Dennis Ritchie and Ken Thompson at AT&T bell Lab. First, they used to call it as B language then later they made some improvement into it and renamed it as C and its superscript as C++ which was invented by Dr. Stroustroupe.

Is C language easy?

C is a general-purpose language that most programmers learn before moving on to more complex languages. From Unix and Windows to Tic Tac Toe and Photoshop, several of the most commonly used applications today have been built on C. It is easy to learn because: A simple syntax with only 32 keywords.

Is C programming hard?

C is more difficult to learn than JavaScript, but it's a valuable skill to have because most programming languages are actually implemented in C. This is because C is a “machine-level” language. So learning it will teach you how a computer works and will actually make learning new languages in the future easier.


1 Answers

Progress<T> captures current SynchronizationContext when created. If there is no SynchronizationContext (like in console app) - progress callbacks will be scheduled to thread pool threads. That means multiple callbacks can even run in parallel, and of course order is not guaranteed.

In UI applications, posting to synchronization context is roughly equivalent to:

  1. In WPF: Dispatcher.BeginInvoke()

  2. In WinForms: Control.BeginInvoke

I'm not working with WinForms, but in WPF, multiple BeginInvoke with the same priority (and in this case they are with the same priority) are guaranteed to execute in order they were invoked:

multiple BeginInvoke calls are made at the same DispatcherPriority, they will be executed in the order the calls were made.

I don't see why in WinForms Control.BeginInvoke might execute our of order, but I'm not aware of a proof like I provided above for WPF. So I think in both WPF and WinForms you can safely rely on your progress callbacks to be executed in order (provided that you created Progress<T> instance itself on UI thread so that context could be captured).

Site note: don't forget to add ConfigureAwait(false) to your ReadAsync and WriteAsync calls to prevent returning to UI thread in UI applications every time after those awaits.

like image 161
Evk Avatar answered Oct 23 '22 10:10

Evk