Windows如何执行I/O操作

以读取磁盘文件类FileStream为例 ,展示下同步和异步I/O的执行流程

同步IO

【CLR】C#异步I/O_C#


  1. 调用FileStream类的Read方法后,你的线程将从托管代码转为win32用户模式代码。并调用win32的ReadFile函数,此函数将会非配一个叫做I/O请求包(I/O Request Packet,IRP)的数据结构,这个结构里包括:文件句柄,文件中的偏移量,Byte[]数组地址等信息。
  2. 然后ReadFile会将你的线程从用户模式变为内核模式,向内核传递IRP数据,从而调用windows内核。
  3. 根据IRP中的句柄,win内核就知道将IRP传给哪个硬件,调用哪个硬件驱动。驱动程序内部维护了一个IRP队列。(在IRP请求完成期间,发出IRP请求的线程将阻塞被切换到睡眠状态,它占用的内存也没有被释放)
  4. IRP完成之后 ,操作系统会唤醒你的线程 ,并把它调度给一个cpu。
  5. 然后从内核模式切换到用户模式。
  6. 再返回至托管代码。

异步IO

【CLR】C#异步I/O_async_02

与同步IO的区别是,在IRP执行期间,你的线程会立即返回(4、5、6),并进入到线程池。等IRP执行结束之后 ,会将这个IRP放入到线程池队列中(a),线程池中的线程会提取IRP并继续完成任务(b)。

同步IO因为会阻塞,所以设计服务器程序时需要多加注意,又可能会耗尽线程数。当大量的线程恢复时,又会因为数量超过了CPU的内核数,而导致系统频发进行线程上下文切换。

使用异步IO的优点:


  • 减少程序的线程数,提供GC效率。因为每次GC之前都需要挂起所有的线程,并且遍历所有线程的栈并找到对应的根进行清理。线程少了,自然速度就快了。
  • 减少程序线程数,增加vs的调试性能。因为当遇到断点的时候,windows也会挂起所有的线程,断点执行之后,又会恢复所有的线程。
  • UI不会阻塞,可以及时响应。

异步函数

主要是async和await,怎么用,教程很多,就不细说了。

有如下几点限制需要注意:


  • Main方法不能加async(因为直接返回就相当于整个程序就结束了),构造函数、属性、事件不能加async
  • asyn方法上不能使用out和ref参数
  • 不能在​​catch​​​、​​finally​​​、​​unsafe​​代码块中使用await操作符。
  • 不能在await之前获取锁,并在await之后释放,即不能lock await。因为前后是两个线程。

TIPS:

  1. 如果想等所有的Task都执行完成之后,再进行处理。可以使用以下类似代码:
//...
string[] responses=await Task.WhenAll(tasks);
//...
  1. 如果想完成一个Task就处理一个,可以用以下类似代码:
while(tasks.Count>0)
{
var req=await Task.WhenAny(tasks);
tasks.Remove(req);
//process req
}



死锁

对于GUI程序来说,因为await之后的代码会切到UI线程执行,所以有可能会出现死锁情况,如下:

class WpfForm:Windos
{
protected override void OnActivated(EventArgs e)
{
//同步的方式获取结果,UI线程将阻塞在这里
string http=GetHttp().Result;
base.OnActivated(e);
}
async Task<string> GetHttp()
{
var msg=await HttpClient.GetAsync("http://xxx.com");
//永远不会执行到这里,因为UI线程已经被阻塞了,这里无法切换到UI线程。
return await msg.Content.ReadStringAsync();
}
}

解决方式是所有的await的对象上 都使用​​ConfigureAwait(false)​​,如下:

class WpfForm:Windos
{
protected override void OnActivated(EventArgs e)
{
//同步的方式获取结果,UI线程将阻塞在这里
string http=GetHttp().Result;
base.OnActivated(e);
}
async Task<string> GetHttp()
{
var msg=await HttpClient.GetAsync("http://xxx.com").ConfigureAwait(false);
//这里的代码将由线程池中的线程完成Task后直接执行(与执行Task的是同一个线程),而不是UI线程
return await msg.Content.ReadStringAsync().ConfigureAwait(false);
}
}

当ConfigureAwait(false)的时候​​await​​操作符将不再查询原线程的​​SynchronizationContext​​对象,新 线程直接继续执行。

另外一种 解决方式是 将GetHttp()的代码,用Task.Run包起来,这样就想当于起了新线程操作 ,GetHttp也不再是异步函数。

怎么取消正在执行的I/O异步操作

windows一般没有提供取消正在执行的异步操作,因为如果向服务器请求了1000个字节,然后又不想要了,windows其实没办法告诉服务器你已经后悔不想要了,只能让你默默接受,接受完了让你自己手动丢弃。但是我们可以通过程序的方式,进行控制,当不想要时,让程序直接返回。

参考以下代码来实现​​Task<TResult>​​的扩展方法:

private struct Void {}//因为TaskCompletionSource类都是泛型的,不得已定义个这
private static async Task<TResult> WithCancellation<TResult>(this Task<TResult> originalTask, CancellationToken ct)
{
var cancelTask=new TaskCompletionSource<Void>();
using(ct.Register(t=>((TaskCompletionSource<Void>)t).TrySetResult(new Void(),cancelTask))
{
var any=await Task.WhenAny(originalTask,cancelTask);
if(any==cancelTask.Task)
ct.ThrowIfCancellationRequested();
}
//等待原始任务(以同步方式)
return await originalTask;
}

使用:

var ct=new CancellationTokenSource(5000).Token;
try
{
await Task.Delay(10000).WithCancellation(ct);
}
catch(OperationCanceledException)
{
//task canceled
}