异步陷阱之IO篇

很多教程和资料都强调流畅的用户体验需要异步来辅助,核心思想就是保证用户前端的交互永远有最高的优先级,让一切费时的逻辑通通放到后台,等到诸事完备,通知一下前端给个提示或者继续下一步。随着.NET发展,async和await关键字的推广,Task Parallel Library (TPL)的稳步发展, 异步编程也越来越多的被重视和采用,很多时候非常便利的解决各种性能问题,但同时也带来了很多的陷阱。​​

这里我抛出一个实际项目中遇到的陷阱,先简单交代一下故事背景:SpreadJS产品有一个Excel IO部件,是一个ASP.NET MVC Web API(MVC4)应用,用来导入Excel文件到SpreadJS中;其工作过程是客户端先上传Excel文件,服务器端接收文件后读出内容,以SpreadJS特有的JSON格式回传给客户端。很长一段时间工作正常,直到某一天有一个“大神”级的客户反馈他在使用Excel IO过程中会一定几率随机出现导入失败,具体的表现是在返回的JSON数据中提示有IO错误,好吧,附上用户场景的代码片段(略去了脚本引用,DOM以及其他机密代码):

$(document).ready(function() {
// initialize 10 spreadjs widgets
for(var i = 0; i < 10; i++) {
$("#ss_" + i).wijspread({ sheetCount: 2 });
}
// import handler
$("#importButton").click(function() {
for(var i = 0; i < 10; i++) {
importToSpread("ss" + i);
}
});
// import process
function importToSpread(target) {
var formData = new FormData();
formData.append("file", $("#importingExcelFile").get(0).files[0]);
formData.append("ExcelOpenFlags", "NoFlagsSet");
formData.append("TextFileOpenFlags", "None");
formData.append("Password", "");
$.ajax( {
url: "http://your.excelio.path/xsapi/import",
type: "POST",
success: function(data, textStatus, jqXHR) {
$("#" + target).wijspread("spread").fromJSON(JSON.parse(jqXHR.responseText).spread);
},
data: formData,
contentType: false,
processData: false,
headers: { "Accept": "application/json" }
});
}
});

也许各位看官可能有话说了:这明显的穷折腾么,有这么把一个文件重复导入10次的实际场景吗?嗯,这是一个社会工程学问题,略过,呵呵。​

根据用户的代码,可以分析得到一些关键信息: 
1、用户在很短时间内快速提交了多个请求并上传文件; 
2、返回结果会随机出现IO错误; 
由此可以得出结论:应该是服务器处理上传的Excel文件时,某个文件在特定情况下不可用,从而导致处理程序抛出IO异常。什么情况会导致IO不可用呢?似乎一下子还真无从下手,作为开发人员,最容易想到的方法就是祭出IDE,直接挂上调试器,只要捕获到这个IO异常就好了。经过几次尝试,终于看到了IO异常了,如下图:

看来前面的分析是对的,文件在特定 情况不可用,但是为什么不可用呢?从上面的IO异常信息可以看出,这个文件是ASP.NET临时保存的上传文件。在ASP.NET WEB API中,处理上传文件的思路和方法如下:

var root = HttpContext.Current.Server.MapPath("~/App_Data");
var provider = new MultipartFormDataStreamProvider(root);
try {
await Request.Content.ReadAsMultipartAsync(provider);
} catch (Exception ex) {
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex);
}
var file = provider.FileData.FirstOrDefault();
// File.OpenRead(file.LocalFileName) // may get exception here

从这个片段很容易分析出一下两种可能导致文件IO的情况: 
1、文件的LocalFileName不唯一 
2、读取上传内容的异步操作结束但是文件还没有释放 
显然,第一条可以排除,因为异常信息里可以看到文件的名字有一个GUID,基本可以保证绝对唯一,所以,问题肯定发生在这里的异步处理。

为了深入的搞清楚发生了什么,我查看了ReadAsMultipartAsync的源代码,这里面会调用MultipartFormDataStreamProvider上的GetStream方法来处理上传的文件:

// ... 略去参数处理
string localFileName = this.GetLocalFileName(headers);
str = Path.Combine(this._rootPath, Path.GetFileName(localFileName));
// ... 略去部分无关逻辑
MultipartFileData item = new MultipartFileData(headers, str);
this._fileData.Add(item);
return File.Create(str, this._bufferSize, FileOptions.Asynchronous);

这里调用GetLocalFileName来获取临时文件名,很清楚的使用了Guid.NewGuid()来保证文件名永远不会重复;焦点转到最后一句返回一个可写的FileStream,注意这里的第三个参数是FileOptions.Asynchronous,就是说,这个FileStream实际是异步IO,但是内部处理逻辑没有等待这个结果就直接走后续的逻辑了,这样导致在服务器运行在高IO并发的情况就很容易发生IO异常。

以上分析了问题,但如何解决呢(某PM话外音:那谁谁,快点啊,客户催着呢),很简单,去除调这个异步IO就可以了,好吧,代码一点也不简单,重写这个GetStream方法,保证获取的FileStream使用同步,虽然一定程度降低了性能,但好歹能解决问题。

参考示例工程代码:下载地址

更新补充:在ASP.NET MVC 5中重写了ReadAsMultipartAsync所在的整个类,已经修复了这个问题(至少我试过同时1000次毫无压力),参考示例中AsyncIoTrap_v5工程。

备注:昨天在OSChina上推出了Wijmo 5 《jQuery UI 组件集 Wijmo 五年最大更新,Mobile First!》。但是本次发布的Wijmo 5 Beta版本未包含SpreadJs。

异步陷阱之IO的更多相关文章

  1. 异步陷阱之IO篇

    很多教程和资料都强调流畅的用户体验需要异步来辅助,核心思想就是保证用户前端的交互永远有最高的优先级,让一切费时的逻辑通通放到后台,等到诸事完备,通知一下前端给个提示或者继续下一步.随着.NET发展,a ...

  2. 异步非阻塞IO的Python Web框架--Tornado

    Tornado的全称是Torado Web Server,从名字上就可知它可用作Web服务器,但同时它也是一个Python Web的开发框架.最初是在FriendFeed公司的网站上使用,FaceBo ...

  3. 转一贴,今天实在写累了,也看累了--【Python异步非阻塞IO多路复用Select/Poll/Epoll使用】

    下面这篇,原理理解了, 再结合 这一周来的心得体会,整个框架就差不多了... http://www.haiyun.me/archives/1056.html 有许多封装好的异步非阻塞IO多路复用框架, ...

  4. Python异步非阻塞IO多路复用Select/Poll/Epoll使用,线程,进程,协程

    1.使用select模拟socketserver伪并发处理客户端请求,代码如下: import socket import select sk = socket.socket() sk.bind((' ...

  5. NodeJS示例异步式(Asynchronous)IO与同步式Synchronous)IO

    理解IO      IO(Input/Output)通常是指计算机线程进行慈磁盘读写或者网络通信时的一种行为.   同步式(Synchronous)IO和异步式(Asynchronous )IO   ...

  6. nodejs的异步非阻塞IO

    简单表述一下:发启向系统IO操作请求,系统使用线程池IO操作,执行完放到事件队列里,node主线程轮询事件队列,读取结果与调用回调.所以说node并非真的单线程,还是使用了线程池的多线程. 上个图看看 ...

  7. swoole与php协程实现异步非阻塞IO开发

    “协程可以在遇到阻塞的时候中断主动让渡资源,调度程序选择其他的协程运行.从而实现非阻塞IO” 然而php是不支持原生协程的,遇到阻塞时如不交由异步进程来执行是没有任何意义的,代码还是同步执行的,如下所 ...

  8. suging闲谈-netty 的异步非阻塞IO线程与业务线程分离

    前言 surging 对外沉寂了一段时间了,但是作者并没有闲着,而是针对于客户的需要添加了不少功能,也给我带来了不少外快收益, 就比如协议转化,consul 的watcher 机制,JAVA版本,sk ...

  9. [Flask] 异步非阻塞IO实现

    Flask默认是不支持非阻塞IO的,表现为: 当 请求1未完成之前,请求2是需要等待处理状态,效率非常低. 在flask中非阻塞实现可以由2种: 启用flask多线程机制 # Flask from f ...

随机推荐

  1. VB.NET之错误异常处理

    相对于VB而言,VB.NET中引入了很多特色.当中最吸引我的就是引入了结构化异常处理. 尽管VB.NET仍然支持OnError Goto类型的异常处理,可是这样做并非非常好.相比而言,结构化异常处理更 ...

  2. NSIS:卸载加密码示例

    原文 NSIS:卸载加密码示例 最近有几个同学问我关于卸载时加密码的问题,其实很简单,懂点基础就可以根据安装加密码那篇文章http://www.flighty.cn/html/bushu/201009 ...

  3. Windows下一个MySQL有些错误的解决方法

    1.无论是什么提示.我们有一个直接看错误日志.由于它描述了最具体描述错误日志. 于MySQL安装文件夹中找到 my.ini简介 看日志保存路径 2. 我的错误是[ERROR] Fatal error: ...

  4. CentOS7 安装zookeeper

    本章介绍CentOS的初始配置与zookeeper的安装. www.apache.org/dist/上可以下载Hadoop整个生态环境的组件,我下的Zookeeper3.4.6版本 我一般都是在一个虚 ...

  5. 接收一个IT招聘促销信息,试着想参加,有兴趣的可以携手并进,共同。

    时隆重举行! 招聘的企业: (个人认为,中智.也买酒还是非常有吸引力的) watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbHdia2Zj/font/5a6L ...

  6. C#从SQL server数据库中读取l图片和存入图片

    原文:C#从SQL server数据库中读取l图片和存入图片 本实例主要介绍如何将图片存入数据库.将图片存入数据库,首先要在数据库中建立一张表,将存储图片的字段类型设为Image类型,用FileStr ...

  7. 多线程相互排斥--mutex(二)

    不知道大家对多线程或多进程间的同步相互排斥的控制机制了解的怎么样,事实上有非常多种方法能够实现这个目的,可是这些方法事实上由4种最主要的方法实现.这4种最主要的方法详细定义例如以下:在这有讲得不正确的 ...

  8. 【网络流量-二部图最大匹配】poj3041Asteroids

    /* 这个问题将是每行一个x作为节点x,没有列y作为节点y,障碍物的坐标xy来自x至y的 边缘.图建的问题后,变成,拿得最少的点,因此,所有这些点与相邻边缘,即最小 点覆盖,与匈牙利算法来解决. -- ...

  9. 经典算法题每日演练——第十一题 Bitmap算法

    原文:经典算法题每日演练--第十一题 Bitmap算法 在所有具有性能优化的数据结构中,我想大家使用最多的就是hash表,是的,在具有定位查找上具有O(1)的常量时间,多么的简洁优美, 但是在特定的场 ...

  10. 经典算法题每日演练——第七题 KMP算法

    原文:经典算法题每日演练--第七题 KMP算法 在大学的时候,应该在数据结构里面都看过kmp算法吧,不知道有多少老师对该算法是一笔带过的,至少我们以前是的, 确实kmp算法还是有点饶人的,如果说红黑树 ...