【问题标题】:FileStream.ReadAsync blocks UI if useAsync is true, but doesn't block UI if it's false如果 useAsync 为 true,FileStream.ReadAsync 会阻止 UI,但如果为 false,则不会阻止 UI
【发布时间】:2020-03-02 11:41:00
【问题描述】:

我在这个FileStream 构造函数中读到了useAsync 参数:

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

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

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 属性设置的值将显示在表单上。但是如果我将useAsync: true 添加到FileStream 构造函数调用中,则文本框只显示“finished”。文本“byte array made”永远不会显示出来。

文件长度为 1 GB。

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

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

这是为什么?

【问题讨论】:

    标签: c# winforms asynchronous


    【解决方案1】:

    违反直觉的行为是我们通常认为的“异步”与 Windows 认为的“异步”之间的差异的结果。前者一般的意思是“去做这件事,做完后再回来找我”。对于 Windows,“异步”实际上翻译为“重叠 I/O”,这是一种说法,即“它可能是异步的”。

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

    在手头的示例中,文件的全部内容(显然……在我的测试中就是这种情况)在文件系统缓存中可用。 同步读取缓存数据(请参阅Asynchronous Disk I/O Appears as Synchronous on Windows),因此您所谓的“异步”操作同步完成。

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

    因为在这种情况下您使用的是线程池线程,并且因为排队工作项总是涉及等待完成,因此无法同步完成,所以您会得到您期望的异步行为。底层 I/O 操作是同步的,但您看不到这一点,因为您调用了一个根据定义提供异步操作的方法,并且它通过线程池执行此操作,线程池本质上是异步的。

    请注意,即使在构造函数中使用useAsync: true,至少有几种方法可以让您看到预期的异步行为,这两种方法都涉及文件不在缓存中。第一个很明显:在上次启动后甚至没有读取文件的情况下测试代码。第二个不是很明显。事实证明,除了为 FileOptions 定义的值之外,标志中还允许使用另一个值(一个其他值):0x20000000。这对应于名为 FILE_FLAG_NO_BUFFERING 的本机 CreateFile() 函数标志。

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

    不过要小心:这是有代价的。缓存的 I/O 操作通常比未缓存的要快很多。根据您的情况,禁用缓存可能会严重影响整体性能。同样禁用异步 I/O。允许 Windows 使用重叠 I/O 通常是一个的主意,并且会提高性能。

    如果由于重叠的 I/O 操作同步完成而导致 UI 变得无响应的问题,最好将该 I/O 移动到工作线程,但在创建 FileStream 时仍传递 useAsync: true对象。您将产生工作线程的开销,但对于任何非常长的 I/O 操作,与允许缓存重叠 I/O 操作所获得的性能改进相比,这将是微不足道的。

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

    • 如果文件不存在则创建文件
    • 当程序关闭时,如果文件是在临时目录中创建的,则删除该文件
    • 显示当前时间,提供有关 UI 是否被阻止的一些反馈
    • 显示有关线程池的一些状态,以便查看工作线程何时变为活动状态(即处理文件 I/O 操作)
    • 有几个复选框,可让您在不重新编译代码的情况下更改操作模式

    观察有用的东西:

    • 当两个复选框都未选中时,I/O 总是异步完成,并显示正在读取的字节数的消息。请注意,在这种情况下,活动工作线程数会增加。
    • 当检查987654337 disable cache 987654338 @未选中,I / O几乎始终同步完成,状态文本未更新
    • 如果两个复选框都被选中,I/O 总是异步完成;没有明显的方法可以将此与线程池中异步执行的操作区分开来,但不同之处在于使用了重叠 I/O,而不是工作线程中的非重叠 I/O。注意:通常,如果您在禁用缓存的情况下进行测试,那么即使您重新启用缓存(取消选中“禁用缓存”),下一个测试仍将异步完成,因为缓存尚未恢复。

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

    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;
    

    解决作为 cmets 发布的后续问题:

    1. useAsync 和 FileOption.Asyncronous 有什么区别

    没有。 bool 参数的重载只是为了方便。它的作用完全相同。

    1. 我应该什么时候使用异步方法:使用异步方法和使用异步:正确?

    当您想要增加重叠 I/O 的性能时,您应该指定useAsync: true

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

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

    您似乎在质疑我的说法,即在FileOptions 参数中包含FILE_FLAG_NO_BUFFERING 将导致ReadAsync() 异步完成(这将通过禁用文件系统缓存的使用来实现)。

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

    需要注意的是,该标志的实际含义并不是“导致ReadAsync()异步完成”。这只是我观察到使用该标志的副作用。缓存并不是导致ReadAsync() 同步完成的唯一条件,因此即使使用该标志,您仍然完全有可能看到ReadAsync() 同步完成。

    无论如何,我认为这都不是真正的问题。我不认为使用FILE_FLAG_NO_BUFFERING 实际上是一个好主意。我已将其包含在此讨论中作为探索ReadAsync() 同步完成的原因的一种方式。我建议使用该标志通常是个好主意。

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

    在某些情况下,这可能会导致整体吞吐量略微降低,这仅仅是因为线程上下文切换。但与文件 I/O 本身相比,这种切换非常便宜,只要 UI 保持响应,用户甚至都不会注意到。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-07-11
      • 2011-03-27
      • 2012-06-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-06-29
      • 1970-01-01
      相关资源
      最近更新 更多