FileStream.ReadAsync 会阻止 UI(如果使用异步为 true),但如果为 false,则不会阻止 UI



我了解了这个FileStream构造函数中的useAsync参数:

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

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

byte[] data;
FileStream fs;
public Form1()
{
InitializeComponent();
fs = new FileStream(@"C:UsersiPDocumentsVisual Studio 2015ProjectsConsoleApplication32ConsoleApplication32binDebughello.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启用时,我希望ReadAsync()方法同步完成,阻塞UI线程,并且不允许在I/O操作完成之前更新文本框。

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

为什么会这样?

反直觉行为是我们通常认为的"异步"和Windows认为的"非同步"之间差异的结果。前者的意思通常是"去做这个,做完后再来找我"。对于Windows,"异步"实际上翻译为"重叠I/O",这是一种表示"它可能是异步的"的方式。

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

在手头的示例中,文件的整个内容(显然在我的测试中就是这样)在文件系统缓存中可用。缓存数据被同步读取(请参阅异步磁盘I/O在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通常是一个好的想法,可以提高性能。

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

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

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

观察有用的东西:

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

以下是示例代码(首先是用户代码,然后是设计器在最后生成的代码):

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;

为了解决作为评论发布的后续问题:

  1. useAsync和FileOption.Ancronous之间有什么区别

无。bool参数的过载只是为了方便。它做的事情完全一样。

  1. 我应该在什么时候使用Async:false和Async:true方法

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

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

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

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

我不能告诉你你的电脑上发生了什么。一般来说,我希望它和我电脑上的一样,但不能保证。我可以告诉您的是,在我的测试中,通过使用FILE_FLAG_NO_BUFFERING禁用缓存对于使ReadAsync()异步完成是100%可靠的。

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

不管怎样,我认为这并不是真正的问题。我不认为使用FILE_FLAG_NO_BUFFERING实际上是个好主意。在本次讨论中,我仅将作为探索ReadAsync()同步完成的原因的一种方式。我而不是建议使用该标志是一个好主意。

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

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

相关内容

最新更新