如果useAsync为true,则FileStream.ReadAsync阻止UI,但如果为false,则不阻止UI [英] FileStream.ReadAsync blocks UI if useAsync is true, but doesn't block UI if it's false

查看:45
本文介绍了如果useAsync为true,则FileStream.ReadAsync阻止UI,但如果为false,则不阻止UI的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在此 FileStream 构造函数中,我了解了 useAsync 参数:

I read about the useAsync parameter in this FileStream constructor:

我试图在Winforms应用程序中使用 FileStream.ReadAsync()方法,如下所示:

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

使用以上内容,在窗体上显示在调用 ReadAsync()之前和之后为 textBox1.Text 属性设置的值.但是,如果我在 FileStream 构造函数调用中添加 useAsync:true ,则文本框仅显示完成" .文本制造的字节数组" 永远不会显示.

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.

文件长度为1 GB.

我希望启用异步I/O后, ReadAsync()方法将异步完成,从而允许UI线程在完成I/O操作之前更新文本框.相反,当启用异步I/O时,我希望 ReadAsync()方法同步完成,从而阻塞UI线程并且不允许更新文本框在I/O操作完成之前.

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.

然而,相反的情况似乎发生了.启用异步I/O会阻止UI线程,而禁用异步I/O则会使I/O操作异步完成并更新UI.

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.

这是为什么?

推荐答案

违反直觉的行为是我们通常认为的异步"与Windows认为的异步"之间差异的结果.前者通常意思是去做,待完成后再回来找我".对于Windows,异步"实际上转换为重叠的I/O",这是一种表示它可以可以是异步的"的方式.

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".

换句话说,在使用Windows时,启用的异步"操作(即重叠的I/O")是告诉Windows您的代码能够处理异步结果的方式.它不是 promise 异步结果,它只是意味着,如果Windows决定某个操作应该异步完成,则它可以依靠您的代码来优雅地处理该操作.否则,它将隐藏代码中的所有异步行为.

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.

当将 useAsync:false 传递给 FileStream 构造函数时,您告诉 FileStream 对象在没有重叠I/O的情况下运行.与您可能会想到的相反–您是说所有操作都应同步完成—事实并非如此.您只是在操作系统中禁用了底层的异步行为.因此,当您调用诸如 BeginRead() ReadAsync()之类的异步方法(前者实质上只是调用后者)时, FileStream 对象仍然提供异步行为.但这是通过使用线程池中的工作线程来实现的,该线程反过来又从文件中同步读取.

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.

由于在这种情况下使用线程池线程,并且由于排队工作项始终涉及等待完成,因此无法同步完成,因此您将获得预期的异步行为.底层的I/O操作是 synchronous ,但是您看不到它,因为您调用的方法根据定义提供了异步操作,并且它是通过线程池实现的,而线程池本质上是异步的.

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.

请注意,即使在构造函数中使用 useAsync:true ,您仍然至少可以通过两种方式看到期望的异步行为,这两种方式都涉及文件不在缓存.第一个很明显:自上次启动以来,即使没有读取文件,也无需测试文件即可测试代码.第二个不太明显.事实证明,除了 FileOptions 的已定义值外,标志中还允许使用其他值(并且 仅另一个值): 0x20000000.这对应于名为 FILE_FLAG_NO_BUFFERING 的本机 CreateFile()函数的标志.

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.

如果将该标志与 FileOptions.Asynchronous 值一起使用,则会发现 ReadAsync()实际上将异步完成.

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

但是要小心:这是有代价的.缓存的I/O操作通常比未缓存的要快很多.根据您的方案,禁用缓存可能会严重损害整体性能.同样禁用异步I/O.允许Windows使用重叠的I/O通常是一个好主意,它将提高性能.

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.

如果由于重叠的I/O操作同步完成而导致UI无法响应的问题,最好将I/O移至工作线程,但仍要传递 useAsync:true FileStream 对象时使用code>.您将产生辅助线程的开销,但是对于任何相当长的I/O操作,与允许缓存的重叠I/O操作获得的性能提高相比,这将是无关紧要的.

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.

就其价值而言,由于我没有一个1 GB的文件可用于测试,并且因为我想对测试和状态信息有更多的控制,所以我从头开始编写了一个测试程序.下面的代码执行以下操作:

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:

  • 创建文件(如果尚不存在)
  • 关闭程序后,如果文件是在temp目录中创建的,则将其删除
  • 显示一天中的当前时间,这会提供有关UI是否被阻止的一些反馈信息
  • 显示有关线程池的某些状态,使人们可以看到工作线程何时处于活动状态(即,处理文件I/O操作)
  • 有几个复选框,允许您在不重新编译代码的情况下更改操作模式

观察有用的东西

  • 如果两个复选框都未选中,则I/O始终异步完成,并显示消息,指出正在读取的字节数.请注意,在这种情况下,活动工作线程数会增加.
  • 在<代码> useAsync 的检查,但<代码>禁用缓存未选中,I/O几乎总是同步完成,状态文本没有更新
  • 如果同时选中了两个复选框,则I/O始终异步完成;没有明显的方法可以将其与线程池中异步执行的操作区分开来,但是不同之处在于,正在使用重叠的I/O,而不是在工作线程中使用不重叠的I/O.注意:通常,如果在禁用缓存的情况下进行测试,则即使重新启用缓存(取消选中禁用缓存"),由于缓存尚未还原,下一次测试仍将异步完成.
  • 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.

这是示例代码(首先是用户代码,然后是Designer生成的代码):

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. useAsync和FileOption.Asyncronous之间有什么区别

没有.为了方便起见,带有 bool 参数的重载就在那里.它做的完全一样.

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

  1. 何时应该使用Asyncronous方法使用useAsync:false和使用useAsync:true?

如果要增加重叠I/O的性能,则应指定 useAsync:true .

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

  1. 如果您将该标志与FileOptions.Asynchronous值一起使用,您会发现ReadAsync()实际上将异步完成.",我认为Asyncronous不会阻止UI,但是当我使用此标志UI时,它仍然会阻止ReadAsync完成

这不是一个真正的问题,而是…

That's not really a question, but…

似乎您在反驳我的说法,即在 FileOptions 参数中包含 FILE_FLAG_NO_BUFFERING 会导致 ReadAsync()异步完成(这可以通过禁用文件系统缓存来实现).

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).

我无法告诉您计算机上发生了什么.通常,我希望它与计算机上的相同,但是并不能保证.我可以告诉你的是,通过使用 FILE_FLAG_NO_BUFFERING 来禁用缓存,在导致 ReadAsync()异步完成的测试中100%可靠

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.

重要的是要注意,标志的实际含义不是导致 ReadAsync()异步完成"..这只是我观察到使用该标志的副作用.缓存不是导致 ReadAsync()同步完成的唯一条件,因此,即使使用该标志,也很有可能仍会看到 ReadAsync()同步完成.

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.

无论如何,我认为这并不是真正的问题.我认为使用 FILE_FLAG_NO_BUFFERING 实际上不是一个好主意.在本讨论中,我仅将其包含在 中,以探讨 ReadAsync()同步完成的原因.我不是建议使用该标志通常是个好主意.

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.

实际上,您通常应该更喜欢重叠I/O的性能提高,因此应该在不禁用缓存的情况下使用 useAsync:true (因为禁用缓存会损害性能).但是,至少在处理非常大的文件时,应将其与结合在工作线程中进行I/O(例如,与 Task.Run()),这样您就不会阻止用户界面.

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.

在某些情况下,仅由于线程上下文切换,这可能导致略微的总体吞吐量降低.但是,与文件I/O本身相比,这种切换非常便宜,而且只要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.

这篇关于如果useAsync为true,则FileStream.ReadAsync阻止UI,但如果为false,则不阻止UI的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆