'FileStream.ReadAsync blocks UI if useAsync is true, but doesn't block UI if it's false

I read about the useAsync parameter in this FileStream constructor:

FileStream(String, FileMode, FileAccess, FileShare, Int32, Boolean)

I tried to use the FileStream.ReadAsync() method in a Winforms app, like this:

byte[] data;
FileStream fs;
public Form1()
{
    InitializeComponent();
    fs = new FileStream(@"C:\Users\iP\Documents\Visual Studio 2015\Projects\ConsoleApplication32\ConsoleApplication32\bin\Debug\hello.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 4096);
     data = new byte[(int)fs.Length];
}

private async void button1_Click(object sender, EventArgs e)
{
    await change();
}

async Task change()
{
    textBox1.Text = "byte array made";
    await fs.ReadAsync(data, 0, data.Length);
    textBox1.Text = "finished";
}

With the above, the value set for the textBox1.Text property both before and after calling ReadAsync() is shown on the form. But if I add useAsync: true to the FileStream constructor call, the text box shows only "finished". The text "byte array made" is never shown.

The file length is 1 GB.

I would expect that when asynchronous I/O is enabled, the ReadAsync() method would complete asynchronously, allowing the UI thread to update the text box before completing the I/O operation. Conversely, when asynchronous I/O is not enabled, I would expect the ReadAsync() method to complete synchronously, blocking the UI thread and not allowing the text box to be updated before the I/O operation completes.

And yet, the opposite seems to happen. Enabling asynchronous I/O blocks the UI thread, while disabling it allows the I/O operation to complete asynchronously and the UI to be updated.

Why is this?



Solution 1:[1]

The counter-intuitive behavior is a consequence of the difference between what we normally think of as "asynchronous" and what Windows thinks of as "asynchronous". The former generally means "go do this, come back to me later when it's done". For Windows, "asynchronous" actually translates to "overlapped I/O", which is a way of saying "it could be asynchronous".

In other words, when dealing with Windows, enabled "asynchronous" operations (i.e "overlapped I/O") is the way of telling Windows that your code is capable of dealing with asynchronous results. It does not promise asynchronous results, it just means that if Windows decides an operation should be completed asynchronously, it can rely on your code to handle that gracefully. Otherwise, it will hide any asynchronous behavior from your code.

In the example at hand, the entire content of your file is (apparently…this was the case in my tests) available in the file system cache. Cached data is read synchronously (see Asynchronous Disk I/O Appears as Synchronous on Windows), and thus your supposedly "asynchronous" operation completes synchronously.

When you pass useAsync: false to the FileStream constructor, you tell the FileStream object to operate without overlapped I/O. Contrary to what you might think — that you are saying that all operations should complete synchronously — that's not the case. You're merely disabling the underlying asynchronous behavior in the operating system. So when you call an asynchronous method like BeginRead() or ReadAsync() (the former essentially just calls the latter), the FileStream object still provides asynchronous behavior. But it does so instead by using a worker thread from the thread pool, which in turn reads from the file synchronously.

Because you're using a thread pool thread in that case, and because queuing work items always involves waiting for completion, and thus cannot complete synchronously, you get the asynchronous behavior you expect. The underlying I/O operation is synchronous, but you don't see that because you called a method that by definition provides for asynchronous operations, and it does that via the thread pool, which is inherently asynchronous.

Note that even with useAsync: true in the constructor, there are at least a couple of ways that you will still see the asynchronous behavior you expect, both of which involve the file not being in the cache. The first is obvious: test the code without having read the file even once since the last boot. The second is not so obvious. It turns out that in addition to the defined values for FileOptions, there is one other value (and only one other value) that is permitted in the flags: 0x20000000. This corresponds to the native CreateFile() function's flag named FILE_FLAG_NO_BUFFERING.

If you use that flag along with the FileOptions.Asynchronous value, you will find that ReadAsync() will in fact complete asynchronously.

Be careful though: there is a cost to this. Cached I/O operations are generally much faster than uncached. Depending on your scenario, disabling caching could significantly impair overall performance. Likewise disabling asynchronous I/O. Allowing Windows to use overlapped I/O is generally a good idea, and will improve performance.

If you have problems with the UI becoming unresponsive, due to overlapped I/O operations completing synchronously, it is probably a better idea to move that I/O to a worker thread but still pass useAsync: true when creating FileStream objects. You'll incur the overhead of the worker thread, but for any significantly long I/O operations, that will be inconsequential compared to the performance improvement gained by allowing cached overlapped I/O operations.

For what it's worth, since I didn't have a 1 GB file lying around to test with, and because I wanted a little more control over the testing and status information, I wrote a test program from scratch. The code below does the following:

  • Creates the file if it doesn't already exist
  • When the program is closed, deletes the file if it was created in the temp directory
  • Displays the current time of day, which gives some feedback as to whether the UI is blocked or not
  • Displays some status about the thread pool, which allows one to see when a worker thread becomes active (i.e. to handle a file I/O operation)
  • Has a couple of check-boxes that allow one to change the mode of operation without recompiling the code

Useful things to observe:

  • When both check-boxes are unchecked, I/O is always asynchronously completed, and the message showing the number of bytes being read is displayed. Note that the active worker thread count goes up in this case.
  • When useAsync is checked but disable cache is unchecked, I/O is almost always completed synchronously, with the status text not updating
  • If both check-boxes are checked, I/O is always asynchronously completed; there's no obvious way to distinguish this from the operation being done asynchronously in the thread pool, but it is different in that overlapped I/O is being used, rather than non-overlapped I/O in a worker thread. NOTE: generally, if you test with caching disabled, then even if you re-enable caching (uncheck "disable caching"), the next test will still complete asynchronously, because the cache hasn't been restored yet.

Here's the sample code (user code first, then the Designer-generated code at the end):

public partial class Form1 : Form
{
    //private readonly string _tempFileName = Path.GetTempFileName();
    private readonly string _tempFileName = "temp.bin";
    private const long _tempFileSize = 1024 * 1024 * 1024; // 1GB

    public Form1()
    {
        InitializeComponent();
    }

    protected override void OnFormClosed(FormClosedEventArgs e)
    {
        base.OnFormClosed(e);
        if (Path.GetDirectoryName(_tempFileName).Equals(Path.GetTempPath(), StringComparison.OrdinalIgnoreCase))
        {
            File.Delete(_tempFileName);
        }
    }

    private void _InitTempFile(IProgress<long> progress)
    {
        Random random = new Random();
        byte[] buffer = new byte[4096];
        long bytesToWrite = _tempFileSize;

        using (Stream stream = File.OpenWrite(_tempFileName))
        {
            while (bytesToWrite > 0)
            {
                int writeByteCount = (int)Math.Min(buffer.Length, bytesToWrite);

                random.NextBytes(buffer);
                stream.Write(buffer, 0, writeByteCount);
                bytesToWrite -= writeByteCount;
                progress.Report(_tempFileSize - bytesToWrite);
            }
        }
    }

    private void timer1_Tick(object sender, EventArgs e)
    {
        int workerThreadCount, iocpThreadCount;
        int workerMax, iocpMax, workerMin, iocpMin;

        ThreadPool.GetAvailableThreads(out workerThreadCount, out iocpThreadCount);
        ThreadPool.GetMaxThreads(out workerMax, out iocpMax);
        ThreadPool.GetMinThreads(out workerMin, out iocpMin);
        label3.Text = $"IOCP: active - {workerMax - workerThreadCount}, {iocpMax - iocpThreadCount}; min - {workerMin}, {iocpMin}";
        label1.Text = DateTime.Now.ToString("hh:MM:ss");
    }

    private async void Form1_Load(object sender, EventArgs e)
    {
        if (!File.Exists(_tempFileName) || new FileInfo(_tempFileName).Length == 0)
        {
            IProgress<long> progress = new Progress<long>(cb => progressBar1.Value = (int)(cb * 100 / _tempFileSize));

            await Task.Run(() => _InitTempFile(progress));
        }

        button1.Enabled = true;
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        label2.Text = "Status:";
        label2.Update();

        // 0x20000000 is the only non-named value allowed
        FileOptions options = checkBox1.Checked ?
            FileOptions.Asynchronous | (checkBox2.Checked ? (FileOptions)0x20000000 : FileOptions.None) :
            FileOptions.None;

        using (Stream stream = new FileStream(_tempFileName, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, options /* useAsync: true */))
        {
            await _ReadAsync(stream, (int)stream.Length);
        }
        label2.Text = "Status: done reading file";
    }

    private async Task _ReadAsync(Stream stream, int bufferSize)
    {
        byte[] data = new byte[bufferSize];

        label2.Text = $"Status: reading {data.Length} bytes from file";

        while (await stream.ReadAsync(data, 0, data.Length) > 0)
        {
            // empty loop
        }
    }

    private void checkBox1_CheckedChanged(object sender, EventArgs e)
    {
        checkBox2.Enabled = checkBox1.Checked;
    }
}

#region Windows Form Designer generated code

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
    this.components = new System.ComponentModel.Container();
    this.button1 = new System.Windows.Forms.Button();
    this.progressBar1 = new System.Windows.Forms.ProgressBar();
    this.label1 = new System.Windows.Forms.Label();
    this.timer1 = new System.Windows.Forms.Timer(this.components);
    this.label2 = new System.Windows.Forms.Label();
    this.label3 = new System.Windows.Forms.Label();
    this.checkBox1 = new System.Windows.Forms.CheckBox();
    this.checkBox2 = new System.Windows.Forms.CheckBox();
    this.SuspendLayout();
    // 
    // button1
    // 
    this.button1.Enabled = false;
    this.button1.Location = new System.Drawing.Point(13, 13);
    this.button1.Name = "button1";
    this.button1.Size = new System.Drawing.Size(162, 62);
    this.button1.TabIndex = 0;
    this.button1.Text = "button1";
    this.button1.UseVisualStyleBackColor = true;
    this.button1.Click += new System.EventHandler(this.button1_Click);
    // 
    // progressBar1
    // 
    this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) 
    | System.Windows.Forms.AnchorStyles.Right)));
    this.progressBar1.Location = new System.Drawing.Point(13, 390);
    this.progressBar1.Name = "progressBar1";
    this.progressBar1.Size = new System.Drawing.Size(775, 48);
    this.progressBar1.TabIndex = 1;
    // 
    // label1
    // 
    this.label1.AutoSize = true;
    this.label1.Location = new System.Drawing.Point(13, 352);
    this.label1.Name = "label1";
    this.label1.Size = new System.Drawing.Size(93, 32);
    this.label1.TabIndex = 2;
    this.label1.Text = "label1";
    // 
    // timer1
    // 
    this.timer1.Enabled = true;
    this.timer1.Interval = 250;
    this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
    // 
    // label2
    // 
    this.label2.AutoSize = true;
    this.label2.Location = new System.Drawing.Point(13, 317);
    this.label2.Name = "label2";
    this.label2.Size = new System.Drawing.Size(111, 32);
    this.label2.TabIndex = 3;
    this.label2.Text = "Status: ";
    // 
    // label3
    // 
    this.label3.AutoSize = true;
    this.label3.Location = new System.Drawing.Point(13, 282);
    this.label3.Name = "label3";
    this.label3.Size = new System.Drawing.Size(93, 32);
    this.label3.TabIndex = 4;
    this.label3.Text = "label3";
    // 
    // checkBox1
    // 
    this.checkBox1.AutoSize = true;
    this.checkBox1.Location = new System.Drawing.Point(13, 82);
    this.checkBox1.Name = "checkBox1";
    this.checkBox1.Size = new System.Drawing.Size(176, 36);
    this.checkBox1.TabIndex = 5;
    this.checkBox1.Text = "useAsync";
    this.checkBox1.UseVisualStyleBackColor = true;
    this.checkBox1.CheckedChanged += new System.EventHandler(this.checkBox1_CheckedChanged);
    // 
    // checkBox2
    // 
    this.checkBox2.AutoSize = true;
    this.checkBox2.Enabled = false;
    this.checkBox2.Location = new System.Drawing.Point(13, 125);
    this.checkBox2.Name = "checkBox2";
    this.checkBox2.Size = new System.Drawing.Size(228, 36);
    this.checkBox2.TabIndex = 6;
    this.checkBox2.Text = "disable cache";
    this.checkBox2.UseVisualStyleBackColor = true;
    // 
    // Form1
    // 
    this.AutoScaleDimensions = new System.Drawing.SizeF(16F, 31F);
    this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
    this.ClientSize = new System.Drawing.Size(800, 450);
    this.Controls.Add(this.checkBox2);
    this.Controls.Add(this.checkBox1);
    this.Controls.Add(this.label3);
    this.Controls.Add(this.label2);
    this.Controls.Add(this.label1);
    this.Controls.Add(this.progressBar1);
    this.Controls.Add(this.button1);
    this.Name = "Form1";
    this.Text = "Form1";
    this.Load += new System.EventHandler(this.Form1_Load);
    this.ResumeLayout(false);
    this.PerformLayout();

}

#endregion

private System.Windows.Forms.Button button1;
private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Timer timer1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.CheckBox checkBox1;
private System.Windows.Forms.CheckBox checkBox2;

To address the follow-up questions posted as comments:

  1. what is differences between useAsync and FileOption.Asyncronous

None. The overload with the bool parameter is just there for convenience. It does exactly the same thing.

  1. when should i useAsync : false with Asyncronous methods and useAsync : true?

When you want the added performance of overlapped I/O, you should specify useAsync: true.

  1. " If you use that flag along with the FileOptions.Asynchronous value, you will find that ReadAsync() will in fact complete asynchronously. " , i think that Asyncronous dont block UI but when i use this flag UI still block untill ReadAsync finish

That's not really a question, but…

It seems like you are disputing my statement that including the FILE_FLAG_NO_BUFFERING in the FileOptions parameter will cause ReadAsync() to complete asynchronously (which it would do by disabling the use of the file system cache).

I can't tell you what happens on your computer. Generally, I'd expect it to be the same as on my computer, but there are no guarantees. What I can tell you is that disabling caching, by using FILE_FLAG_NO_BUFFERING, is 100% reliable in my tests for causing ReadAsync() to complete asynchronously.

It is important to note that the actual meaning of the flag is not "cause ReadAsync() to complete asynchronously". That's simply the side-effect I observe of using that flag. Caching is not the only condition that would cause ReadAsync() to complete synchronously, so it's entirely possible that even when using that flag, you would still see ReadAsync() complete synchronously.

Regardless, I think that's all of no real concern. I don't think that using the FILE_FLAG_NO_BUFFERING is actually a good idea. I have included that in this discussion only as a way to explore the reason why ReadAsync() completes synchronously. I am not suggesting that it's a good idea in general to use that flag.

You should in fact generally prefer the increased performance of overlapped I/O, and so should use useAsync: true without disabling caching (because disabling caching will harm performance). But you should combine that with also doing the I/O in a worker thread (e.g. with Task.Run()), at least when you are dealing with very large files, so that you don't block the UI.

This may in some cases result in slightly less overall throughput, simply because of the thread context switching. But that switching is very cheap compared to file I/O itself and as long as the UI remains responsive, is not something a user will even notice.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1