真正异步 I/O
使用 IOCP 的轻量级、高性能、易于使用的异步流复制方法,具有进度、吞吐量跟踪,并且没有显式线程创建。
此代码是在 Visual Studio 2015 中创建的,因为它是我拥有的,但它应该适用于所有 .NET 版本,从 2.0 开始,甚至 1.1 只需要进行一两处小的更改。不过,您需要自己创建一个 C# 控制台应用程序项目并将源代码文件添加进去。
引言
您的应用程序执行了多少 I/O 操作?
您对优化它进行了多少思考?
我用 C++ 编写代码时与用 C# 编写代码时的思维方式不同。在 C++ 中,代码本身就需要大量的预先思考,所以我花费更多时间考虑优化。另一方面,托管代码之所以吸引人,主要是因为它简单且易于维护,而优化往往会被忽略。尽管如此,文件和网络 I/O 是一个关键领域,无论使用何种编程工具。I/O 操作很慢,而且紧凑的代码在从互联网连接或硬盘驱动器中榨取更多吞吐量方面作用有限。但这并不意味着我们无法通过应用程序来提高其响应速度。
通常,如果您无法加快一个耗时任务的速度,那么至少要让应用程序在执行该任务时能够做其他事情。也就是说,使其异步。这就是我们将要做的,我们将以一种高效且最终易于使用的方式来做到这一点。
背景
大多数开发人员在考虑执行耗时后台任务时会想到线程。线程很棒,但并非没有代价。事实上,当您的线程数量超过核心数量时,您所做的只是在后台和前景代码之间快速切换。您仍然会在驱动程序级别被阻塞,例如硬盘驱动器驱动程序或 TCP/IP 堆栈——只是在另一个单独的线程上被阻塞。更糟的是,您拥有的线程越多,对操作系统任务调度程序的压力就越大。在不需要时创建线程会影响可伸缩性。
现在,如果我告诉您,我们可以执行真正的异步操作,而不是仅仅在后台线程上运行同步操作,您会怎么想?我所说的真正的异步,是指一直到驱动程序,甚至硬件级别——设备本身在后台运行读取操作,我们收到的是一个 IRQ 中断——CPU 在读取完成时触发硬件中断——而不是等待 stream.Read(...) 返回并从另一个线程信号同步对象?您觉得这更有效率吗?事实确实如此。
由此引入 I/O 完成端口(IOCP)——一种在 Windows 机器上执行异步 I/O 操作的强大方法。IOCP 提供真正的无线程异步 I/O,以实现最大效率和可伸缩性。仅在驱动程序通过操作系统将完成信号分派回应用程序时才会使用额外的线程,并且该线程是从几个预分配的线程池中分配的,并自动回收。此外,每个 IOCP 不仅处理一个,而且可能处理大量操作,因此几乎没有可测量的线程开销,而在传统的多线程应用程序中,您可能会看到每操作一到一的线程分配比例。
我不确定有多少人知道 .NET 在 System.IO.Stream.BeginRead 和 BeginWrite 操作的幕后利用了 IOCP,但现在您知道了。事实上,我在这里看到过一些项目直接使用 P/Invoke 调用 Windows IOCP API,这是不幸的,因为它要求使用它的应用程序必须被信任,而且由于额外的封送开销可能比您在 .NET 运行时中发现的要大。这也会使代码难以维护。在此应用程序中,我们将使用 BeginRead 和 BeginWrite 来使用 IOCP。
使用代码
代码非常简单易用。整个核心是一个 Stream 扩展方法:CopyToWithProgress,它返回一个 StreamCopyProgress 对象,您可以使用该对象来监视和控制复制操作的完成。
也许最简单的使用方式是这样的
...
using System.IO;
using BadKitty.IO;
...
Stream source;
Stream destination;
...
// assume source and destination have both created somewhere above and point to their respective I/O.
using (StreamCopyProgress progress = source.CopyToWithProgress(destination, true))
{
// do some work here - the operation is already taking place.
// if you want to cancel it, just call progress.Cancel();
// wait for finish - we don't have to wait, we can use a callback or even poll using the IsFinished property in a loop depending on the scenario, we're just doing it here because we can.
progress.Wait();
// example of polling
// while(!progress.IsFinished)
// {
// Thread.Sleep(1000); // sleep for 1 second
// Console.Write('.');
// }
// Console.WriteLine();
// see the included source code for an example of using the callback feature.
Console.WriteLine("Transfered " + (progress.Transferred / 1024.0).ToString() + "kb in " +
progress.ElapsedSeconds + " seconds @ " + (progress.TransferRate / 1024.0).ToString() + "kbps");
}
关注点
敏锐的读者会注意到 SteamCopyProgress 类中的大部分是由私有字段组成的,这些字段只初始化一次,然后通过属性的 get 访问器进行访问。之所以这样处理,是因为这个对象会通过多个同步上下文传递,并且可能从一个或多个不同的线程调用。为了减少影响性能的锁定需求,有必要将对该对象的写入量降至最低,并且这些写入是通过原子操作完成的。原子操作是原子写入,因此是线程安全的,并且不会干扰读取。这也是为什么进度状态存储为整数而不是枚举。与枚举相比,使用原子操作处理固有数字类型更简单、更安全。
就这样!下次我们结合这里的概念和异步 WebRequest/WebResponse 操作时再见。这是系列文章的一部分,最终将创建一个超快的分段下载器,并带有一些炫酷的 GUI 控件。
为什么是公共领域?
此作品或我的任何提交都没有获得许可。它们是公共领域的。我属于那种奇怪的共产主义者,不热衷于对“知识产权”进行许可。随意使用它。尽情享用。让您的生活变得更好。让别人的生活变得更好。也可以什么都不做。或者用它推翻一个小国家,建立您自己的傀儡政府。随您喜欢。说实话,我只是希望人们能够学习、成长并提高自己的技艺。如果这段代码能帮助一个人做到这一点,那么它就值我编写它的时间。我不关心署名,或者知识产权之类的抽象概念,尽管如果您曾经觉得有必要给我捐款(或者最好是给 Rojava 雄狮倡议捐款),我不会拒绝。=)