Windows如何执行I/O操作
以读取磁盘文件类FileStream为例 ,展示下同步和异步I/O的执行流程
同步IO
- 调用FileStream类的Read方法后,你的线程将从托管代码转为win32用户模式代码。并调用win32的ReadFile函数,此函数将会非配一个叫做I/O请求包(I/O Request Packet,IRP)的数据结构,这个结构里包括:文件句柄,文件中的偏移量,Byte[]数组地址等信息。
- 然后ReadFile会将你的线程从用户模式变为内核模式,向内核传递IRP数据,从而调用windows内核。
- 根据IRP中的句柄,win内核就知道将IRP传给哪个硬件,调用哪个硬件驱动。驱动程序内部维护了一个IRP队列。(在IRP请求完成期间,发出IRP请求的线程将阻塞被切换到睡眠状态,它占用的内存也没有被释放)。
- IRP完成之后 ,操作系统会唤醒你的线程 ,并把它调度给一个cpu。
- 然后从内核模式切换到用户模式。
- 再返回至托管代码。
异步IO
与同步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:
- 如果想等所有的Task都执行完成之后,再进行处理。可以使用以下类似代码:
//...
string[] responses=await Task.WhenAll(tasks);
//...
- 如果想完成一个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
}