这种违反直觉的行为是我们通常认为的“异步”与 Windows 认为的“异步”之间差异的结果。前者通常意味着“去做这件事,完成后再回来找我”。对于 Windows,“异步”实际上翻译为“重叠 I/O”,即“它could是异步的”。
换句话说,在处理 Windows 时,启用“异步”操作(即“重叠 I/O”)是告诉 Windows 您的代码正在运行的方式。capable处理异步结果。它不是promise异步结果,它只是意味着如果 Windows 决定应异步完成某个操作,它可以依靠您的代码来优雅地处理该操作。否则,它将隐藏代码中的任何异步行为。
在当前的示例中,文件的全部内容(显然......这是我的测试中的情况)在文件系统缓存中可用。读取缓存数据同步地 (see 异步磁盘 I/O 在 Windows 上显示为同步 https://support.microsoft.com/en-us/help/156932/asynchronous-disk-i-o-appears-as-synchronous-on-windows),因此您所谓的“异步”操作同步完成。
当你通过时useAsync: false
to the FileStream
构造函数,你告诉FileStream
对象在没有重叠 I/O 的情况下进行操作。与您可能的想法相反 - 您说所有操作都应该同步完成 - 事实并非如此。您只是禁用操作系统中的底层异步行为。所以当你调用像这样的异步方法时BeginRead()
or ReadAsync()
(前者本质上只是调用后者),FileStream
对象仍然提供异步行为。但它是通过使用线程池中的工作线程来实现的,该线程又同步地从文件中读取。
因为在这种情况下您使用的是线程池线程,并且因为排队工作项总是涉及等待完成,因此无法同步完成,所以您会得到您期望的异步行为。底层的I/O操作是同步,但您看不到这一点,因为您调用了一个根据定义提供异步操作的方法,并且它通过本质上是异步的线程池来执行此操作。
请注意,即使有useAsync: true
在构造函数中,至少有几种方法仍然可以看到您期望的异步行为,这两种方法都涉及文件不在缓存中。第一个很明显:自上次启动以来无需读取文件一次即可测试代码。第二个就不那么明显了。事实证明,除了定义的值之外FileOptions
,还有另一个值(并且only标志中允许的另一个值):0x20000000
。这对应于原生CreateFile()
函数的标志名为FILE_FLAG_NO_BUFFERING
.
如果您将该标志与FileOptions.Asynchronous
值,你会发现ReadAsync()
实际上将异步完成。
但要小心:这是有代价的。缓存的 I/O 操作通常是much比未缓存的速度更快。根据您的情况,禁用缓存可能会严重影响整体性能。同样禁用异步 I/O。允许 Windows 使用重叠 I/O 通常是good想法,并将提高性能。
如果由于重叠 I/O 操作同步完成而导致 UI 变得无响应,那么最好将该 I/O 移至工作线程,但仍会通过useAsync: true
创建时FileStream
对象。您将产生工作线程的开销,但对于任何相当长的 I/O 操作,与允许缓存重叠 I/O 操作所获得的性能改进相比,这将是微不足道的。
无论如何,由于我没有 1 GB 的文件可供测试,并且因为我想要对测试和状态信息有更多的控制,所以我从头开始编写了一个测试程序。下面的代码执行以下操作:
- 如果文件尚不存在则创建该文件
- 程序关闭时,删除该文件(如果该文件是在临时目录中创建的)
- 显示当前时间,提供有关 UI 是否被阻止的一些反馈
- 显示有关线程池的一些状态,这允许人们查看工作线程何时变为活动状态(即处理文件 I/O 操作)
- 有几个复选框,允许用户更改操作模式而无需重新编译代码
观察有用的事情:
- 当两个复选框均未选中时,I/O 始终异步完成,并显示显示正在读取的字节数的消息。请注意,在这种情况下,活动工作线程计数会增加。
- When
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;
为了解决作为评论发布的后续问题:
- useAsync 和 FileOptions.Asynchronous 之间有什么区别
没有任何。过载与bool
参数只是为了方便而存在。它的作用完全相同。
- 我什么时候应该在异步方法中使用 Async : false 并使用 Async : true ?
当您想要重叠 I/O 的附加性能时,您应该指定useAsync: true
.
- “如果您将该标志与 FileOptions.Asynchronous 值一起使用,您会发现 ReadAsync() 实际上会异步完成。”,我认为异步不会阻塞 UI,但当我使用此标志时,UI 仍然会阻塞,直到 ReadAsync 完成
这并不是一个真正的问题,但是……
看来你对我的说法有争议,包括FILE_FLAG_NO_BUFFERING
in the FileOptions
参数会导致ReadAsync()
异步完成(这将通过禁用文件系统缓存的使用来完成)。
我无法告诉你你的计算机上发生了什么。一般来说,我希望它与我的计算机上的相同,但不能保证。我什么can告诉你的是禁用缓存,通过使用FILE_FLAG_NO_BUFFERING
,在我的测试中 100% 可靠ReadAsync()
异步完成。
需要注意的是,实际情况meaning国旗的不是"cause ReadAsync()
异步完成”。这就是简单的副作用我观察到使用该标志。缓存并不是唯一会导致ReadAsync()
同步完成,因此完全有可能即使使用该标志,您仍然会看到ReadAsync()
同步完成。
无论如何,我认为这并不是真正值得关注的问题。我不认为使用FILE_FLAG_NO_BUFFERING
实际上是个好主意。我已将其纳入本次讨论中only作为探索原因的一种方式ReadAsync()
同步完成。我是not表明一般来说使用该标志是一个好主意。
事实上,您通常应该更喜欢重叠 I/O 的更高性能,因此应该使用useAsync: true
不禁用缓存(因为禁用缓存会损害性能)。但你应该将其与also在工作线程中执行 I/O(例如,使用Task.Run()
),至少在处理非常大的文件时,这样就不会阻塞 UI。
在某些情况下这可能会导致slightly总体吞吐量较低,仅仅是因为线程上下文切换。但与文件 I/O 本身相比,这种切换非常便宜,并且只要 UI 保持响应,用户甚至不会注意到。