最近要做个简单的类似 CNZZ 和百度统计的统计器,不可避免地遇到 JS 文件异步加载 和 给 JS 文件传参 的问题。
参考了 CNZZ 的代码以后,在 Chrome 的控制台发现以下警告:
A Parser-blocking, cross-origin script, http://s4.cnzz.com/stat.php?***, is invoked via document.write. This may be blocked by the browser if the device has poor network connectivity. See https://www.chromestatus.com/feature/5718547946799104 for more details.
Paul Kinlan 给出了解释,是因为使用了 document.write() 的方式输出了 <script src="***" /> HTML DOM,建议改成 document.appendChild() 或 parentNode.insertBefore(),最好的例子就是 Google Analytics。
<!-- Google Analytics -->
<script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->上述 JavaScript 跟踪代码段可以确保该脚本在所有浏览器中加载和异步执行。
加了一些注释,便于理解,官方英文版。
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
// console.log(window['GoogleAnalyticsObject']) // 'ga'
// console.log(i[r]) // undefined
i[r] = i[r] || function () { // i[r] 就是 window['ga'],定义了一个函数
(i[r].q = i[r].q || []).push(arguments) // 往 ga.q 这个数组中增加一项
},
i[r].l = 1 * new Date(); // 时间戳,写法等同于 new Date().getTime()
// console.log(i[r]) // window['ga'] 就是上面那个 function
a = s.createElement(o), // 创建一个 script 元素
m = s.getElementsByTagName(o)[0]; // 文档中的第一个脚本(文档中肯定至少已有一个脚本了)
a.async = 1; // 异步加载
a.defer = 1; // 兼容旧浏览器(我自己加的)
a.src = g;
m.parentNode.insertBefore(a, m) // 将 a 脚本插入到 m 脚本之前
})(window, document, 'script', 'http://***/***.js', 'ga');
// i s o g r
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');过程是:
创建了一个 <script> 元素,并异步加载 http://***/***.js;初始化了一个全局函数 ga;在 ga() 命令队列中添加了两条命令。
现在我们可以在这个外部 js 中使用 ga.q 这个对象中的数据了,示例:
;(function () {
console.log(ga.q);
})(window);简单补充下,async 是 HTML5 属性,使支持异步加载 JS 文件;defer 只支持 IE,作用类似。
测试异步只需要将 js 文件换成服务端页面,并人为设置 sleep 时间即可,阻塞式调用的话会在加载 js 时暂停后续页面的渲染。
程序设计规范:
【推荐】上传的文件直接保存到文件存储服务(如阿里云 OSS),这样即使被上传了后门 shell,对网站服务器也不会有影响。
否则必须通过文件头来确定文件类型,检查文件十六进制的文件头和文件尾是否合法,并检查文件流中是否包含 php、evel 等字符。
不可直接使用客户端文件名来保存文件,特别是后缀名/扩展名。应生成随机文件名,并通过检验文件头来确定文件类型。必须由程序指定保存目录。
使用 OSS 的应直接上传,不要在 ECS 上临时存放或备份。如必须存放的,应按上述规范操作。
服务器安全设置
CentOS + nginx + PHP:
全站文件取消属性中的“执行”权限(chmod),因为这个“执行”与运行 PHP 无关。而需要上传文件的“目录”需要“执行”权限,原因是需要往该目录创建文件。
仅需要写入的目录或文件设置“写入”权限。如上传图片目录、ThinkPHP 的 Runtime 目录。
凡可写目录或文件均不允许运行 PHP / PY 等 除需要被直接访问的 PHP / PY 文件,其它动态文件均不允许被访问到,在 nginx 的配置文件中添加项,参:https://xoyozo.net/Blog/Details/nginx-location-if,若全站使用统一的入口文件访问,那么设置仅该文件允许运行 PHP 即可。通过 IO 方式被其它文件包含的文件,无需运行 PHP 权限。(“deny all”对 include 起作用,但对 IO 不起作用,因此 Runtime 目录可以继续为 ThinkPHP 提供缓存服务。)这一步非常有用。
使用与 nginx 网站用户不同的用户来部署网站文件,如宝塔面板 PHP 使用 www 用户,那么就使用 root 或其它新用户来上传文件,否则将导致全站目录和文件可写。有条件的建议不同网站使用不同的用户,可防止一个网站被入侵后导致其它网站文件或磁盘上的其它文件被泄露的风险(2022年10月2日从宝塔官方社区获悉,宝塔面板暂不支持使用非 www 用户创建并运行网站)。
Windows Server + IIS + ASP.NET:
配置每个磁盘的安全属性,拒绝“IIS_IUSRS”这个用户组的所有权限。只要设置驱动器即可,子文件夹和文件会自动继承。若运行 .NET Framework 项目,需要设置 C:\Windows\Microsoft.NET\Framework\v*.*.*****\Temporary ASP.NET Files\ 目录可修改写入权限,.NET Core 项目不需要此设置。
为每个网站创建一个新用户,仅隶属于“IIS_IUSRS”。网站根目录安全属性添加该用户,权限选择“读取”。(已测取消“读取与执行”不影响 PHP,“列出文件夹内容”视业务需求开启,建议关闭)。仅需要上传文件的目录或文件设置“修改”、“写入”权限。(修改对应修改文件,写入对应上传文件)
IIS 网站中设置“物理路径凭据”以及应用程序池的“标识”。
IIS 中设置写入目录的“处理程序映射”无脚本。
Repeater
索引 <%# Container.ItemIndex %>
输出项 <%# Container.DataItem %> 或 <%# GetDataItem() %>
ListView
索引 <%# Container.DataItemIndex %>
输出项 <%# GetDataItem() %>
以 fiels 的内容为 <span style="color: red;">你好</span> 为例:
| 语法 | HTML 源代码输出结果 | 浏览器展示结果 | 备注 |
| <%# Eval("field") %> | <span style="color: red;">你好</span> | 你好 | 一般绑定 |
| <%#: Eval("field") %> | <span style="color: red;">你好</span> | <span style="color: red;">你好</span> | HtmlEncode 效果绑定 |
| 成员名称 | 说明 |
|---|---|
| Accepted |
等效于 HTTP 状态 202。 Accepted 指示请求已被接受做进一步处理。 |
| Ambiguous |
等效于 HTTP 状态 300。 Ambiguous 指示请求的信息有多种表示形式。默认操作是将此状态视为重定向,并遵循与此响应关联的 Location 标头的内容。 |
| BadGateway |
等效于 HTTP 状态 502。 BadGateway 指示中间代理服务器从另一代理或原始服务器接收到错误响应。 |
| BadRequest |
等效于 HTTP 状态 400。 BadRequest 指示服务器未能识别请求。如果没有其他适用的错误,或者不知道准确的错误或错误没有自己的错误代码,则发送 BadRequest。 |
| Conflict |
等效于 HTTP 状态 409。 Conflict 指示由于服务器上的冲突而未能执行请求。 |
| Continue |
等效于 HTTP 状态 100。 Continue 指示客户端可能继续其请求。 |
| Created |
等效于 HTTP 状态 201。 Created 指示请求导致在响应被发送前创建新资源。 |
| ExpectationFailed |
等效于 HTTP 状态 417。 ExpectationFailed 指示服务器未能符合 Expect 头中给定的预期值。 |
| Forbidden |
等效于 HTTP 状态 403。 Forbidden 指示服务器拒绝满足请求。 |
| Found |
等效于 HTTP 状态 302。 Found 指示请求的信息位于 Location 头中指定的 URI 处。接收到此状态时的默认操作为遵循与响应关联的 Location 头。原始请求方法为 POST 时,重定向的请求将使用 GET 方法。 |
| GatewayTimeout |
等效于 HTTP 状态 504。 GatewayTimeout 指示中间代理服务器在等待来自另一个代理或原始服务器的响应时已超时。 |
| Gone |
等效于 HTTP 状态 410。 Gone 指示请求的资源不再可用。 |
| HttpVersionNotSupported |
等效于 HTTP 状态 505。 HttpVersionNotSupported 指示服务器不支持请求的 HTTP 版本。 |
| InternalServerError |
等效于 HTTP 状态 500。 InternalServerError 指示服务器上发生了一般错误。 |
| LengthRequired |
等效于 HTTP 状态 411。 LengthRequired 指示缺少必需的 Content-length 头。 |
| MethodNotAllowed |
等效于 HTTP 状态 405。 MethodNotAllowed 指示请求的资源上不允许请求方法(POST 或 GET)。 |
| Moved |
等效于 HTTP 状态 301。 Moved 指示请求的信息已移到 Location 头中指定的 URI 处。接收到此状态时的默认操作为遵循与响应关联的 Location 头。原始请求方法为 POST 时,重定向的请求将使用 GET 方法。 |
| MovedPermanently |
等效于 HTTP 状态 301。 MovedPermanently 指示请求的信息已移到 Location 头中指定的 URI 处。接收到此状态时的默认操作为遵循与响应关联的 Location 头。 |
| MultipleChoices |
等效于 HTTP 状态 300。 MultipleChoices 指示请求的信息有多种表示形式。默认操作是将此状态视为重定向,并遵循与此响应关联的 Location 标头的内容。 |
| NoContent |
等效于 HTTP 状态 204。 NoContent 指示已成功处理请求并且响应已被设定为无内容。 |
| NonAuthoritativeInformation |
等效于 HTTP 状态 203。 NonAuthoritativeInformation 指示返回的元信息来自缓存副本而不是原始服务器,因此可能不正确。 |
| NotAcceptable |
等效于 HTTP 状态 406。 NotAcceptable 指示客户端已用 Accept 头指示将不接受资源的任何可用表示形式。 |
| NotFound |
等效于 HTTP 状态 404。 NotFound 指示请求的资源不在服务器上。 |
| NotImplemented |
等效于 HTTP 状态 501。 NotImplemented 指示服务器不支持请求的函数。 |
| NotModified |
等效于 HTTP 状态 304。 NotModified 指示客户端的缓存副本是最新的。未传输此资源的内容。 |
| OK |
等效于 HTTP 状态 200。 OK 指示请求成功,且请求的信息包含在响应中。这是最常接收的状态代码。 |
| PartialContent |
等效于 HTTP 状态 206。 PartialContent 指示响应是包括字节范围的 GET 请求所请求的部分响应。 |
| PaymentRequired |
等效于 HTTP 状态 402。保留 PaymentRequired 以供将来使用。 |
| PreconditionFailed |
等效于 HTTP 状态 412。 PreconditionFailed 指示为此请求设置的条件失败,且无法执行此请求。条件是用条件请求标头(如 If-Match、If-None-Match 或 If-Unmodified-Since)设置的。 |
| ProxyAuthenticationRequired |
等效于 HTTP 状态 407。 ProxyAuthenticationRequired 指示请求的代理要求身份验证。Proxy-authenticate 头包含如何执行身份验证的详细信息。 |
| Redirect |
等效于 HTTP 状态 302。 Redirect 指示请求的信息位于 Location 头中指定的 URI 处。接收到此状态时的默认操作为遵循与响应关联的 Location 头。原始请求方法为 POST 时,重定向的请求将使用 GET 方法。 |
| RedirectKeepVerb |
等效于 HTTP 状态 307。 RedirectKeepVerb 指示请求信息位于 Location 头中指定的 URI 处。接收到此状态时的默认操作为遵循与响应关联的 Location 头。原始请求方法为 POST 时,重定向的请求还将使用 POST 方法。 |
| RedirectMethod |
等效于 HTTP 状态 303。作为 POST 的结果,RedirectMethod 将客户端自动重定向到 Location 头中指定的 URI。用 GET 生成对 Location 标头所指定的资源的请求。 |
| RequestedRangeNotSatisfiable |
等效于 HTTP 状态 416。 RequestedRangeNotSatisfiable 指示无法返回从资源请求的数据范围,因为范围的开头在资源的开头之前,或因为范围的结尾在资源的结尾之后。 |
| RequestEntityTooLarge |
等效于 HTTP 状态 413。 RequestEntityTooLarge 指示请求太大,服务器无法处理。 |
| RequestTimeout |
等效于 HTTP 状态 408。 RequestTimeout 指示客户端没有在服务器期望请求的时间内发送请求。 |
| RequestUriTooLong |
等效于 HTTP 状态 414。 RequestUriTooLong 指示 URI 太长。 |
| ResetContent |
等效于 HTTP 状态 205。 ResetContent 指示客户端应重置(或重新加载)当前资源。 |
| SeeOther |
等效于 HTTP 状态 303。作为 POST 的结果,SeeOther 将客户端自动重定向到 Location 头中指定的 URI。用 GET 生成对 Location 标头所指定的资源的请求。 |
| ServiceUnavailable |
等效于 HTTP 状态 503。 ServiceUnavailable 指示服务器暂时不可用,通常是由于过多加载或维护。 |
| SwitchingProtocols |
等效于 HTTP 状态 101。 SwitchingProtocols 指示正在更改协议版本或协议。 |
| TemporaryRedirect |
等效于 HTTP 状态 307。 TemporaryRedirect 指示请求信息位于 Location 头中指定的 URI 处。接收到此状态时的默认操作为遵循与响应关联的 Location 头。原始请求方法为 POST 时,重定向的请求还将使用 POST 方法。 |
| Unauthorized |
等效于 HTTP 状态 401。 Unauthorized 指示请求的资源要求身份验证。WWW-Authenticate 头包含如何执行身份验证的详细信息。 |
| UnsupportedMediaType |
等效于 HTTP 状态 415。 UnsupportedMediaType 指示请求是不支持的类型。 |
| Unused |
等效于 HTTP 状态 306。 Unused 是未完全指定的 HTTP/1.1 规范的建议扩展。 |
| UpgradeRequired |
等效于 HTTP 状态 426。 UpgradeRequired 指示客户端应切换为诸如 TLS/1.0 之类的其他协议。 |
| UseProxy |
等效于 HTTP 状态 305。 UseProxy 指示请求应使用位于 Location 头中指定的 URI 的代理服务器。 |
最近做到微信语音时用到,将微信的媒体文件(.amr)下载到自己服务器上,并转码为 .mp3 格式。
下载 FFmpeg for Windows,放在网站目录下,譬如:网站目录/bin/ffmpeg/ffmpeg.exe
转换代码
string amr = HttpRuntime.AppDomainAppPath + media.sLocalPath.Substring(1).Replace("/", @"\");
string mp3 = amr + ".mp3";
string ffmpeg = HttpRuntime.AppDomainAppPath + @"bin\ffmpeg\ffmpeg.exe";
Process p = new Process();
p.StartInfo.FileName = ffmpeg;
p.StartInfo.Arguments = $@"-y -i {amr} -ar {16000} {mp3}";
p.Start();
p.WaitForExit();
p.Close();-y 若有提示用户选择,默认“是”
-ar 采样数,微信的 amr 是 8000 Hz,如果不加这个参数转换后的 mp3 的也是 8000 Hz,但是音质下降严重,所以设成 16000 Hz 才勉强保持了音质。
WaitForExit() 是等待转码完毕才继续往下执行。
IIS 中如果直接“停止”网站,那么打开页面时会显示不友好的“该页无法显示”等界面,如果特殊原因要求不能打开所有页面,但必须有一个友好提示“网站维护中”的页面,那么最土的办法是把网站的所有文件移出来,仅上传一个提示正在维护的网页,并保证是默认首页。其实有一个更加简单有效的解决方案:
在 IIS 中单击该网站,打开“URL 重写”,右侧“添加规则”,选择“空白规则”。名称可以是“网站维护中”,请求的 URL 选择“与模式不匹配”,使用“完全匹配”,模式填入我们的提示页面地址,如“maintaining.html”,操作类型“重定向”,重定向 URL 填入刚才那个页面地址,附加查询字符串不需要,类型选择“临时(307)”,点击右侧“应用”。
好了,该网站的任何请求都会引导到 maintaining.html,而不仅仅是首页的跳转。而且不需要移动网站文件,也保障了不会被人利用网站漏洞攻击篡改网站内容。
补充一点,如果你的 maintaining.html 中调用了网站中的图片、脚本、样式表等,那么需要把模式匹配的方式改为“正则表达式”,并填入正确的正则表达式,以保证这些文件都能被正常请求。
最理想的部署方式是 ClickOnce,但是证书问题会导致安装时提示:
Windows 已保护你的电脑
Windows SmartScreen 筛选器已阻止启动一个未识别的应用。运行此应用可能会导致你的电脑存在安全风险。
这需要一个从 CA 获取的数字证书(http://www.doc88.com/p-785388554071.html)
百科: http://baike.baidu.com/view/1390498.htm
ClickOnce 部署教程: http://www.cnblogs.com/weixing/p/3358740.html
Makecert.exe(证书创建工具): https://msdn.microsoft.com/zh-cn/library/bfsktky3.aspx
如何:为 ClickOnce 应用程序向客户端计算机添加一个受信任的发行者:https://msdn.microsoft.com/zh-cn/library/ms172241.aspx
执行命令:makecert -r -n "CN=嗨秒网" -sv himiao.pvk himiao.cer
Password: LRQelk0l****************FTtJvufI
-r 创建自我签名证书。
-n name 指定主题的证书名称。 此名称必须符合 X.500 标准。 最简单的方法是在双引号中指定此名称,并加上前缀 CN=;例如,-n "CN=myName"。
-sv pvkFile 指定主题的 .pvk 私钥文件。 如果该文件不存在,系统将创建一个。
执行命令:Cert2spc himiao.cer himiao.spc
执行命令:pvk2pfx -pvk himiao.pvk -spc himiao.spc -pfx himiao.pfx -pi LRQelk0l****************FTtJvufI –po LRQelk0l****************FTtJvufI –f
检查目前硬盘状态:fdisk -l
关机并插入新硬盘
对新硬盘分区:fdisk /dev/sdb (假设新硬盘为sdb)
m 显示命令菜单
d 删除一个分区
n 创建一个分区(e 扩展分区;p 主分区)
t 改变分区ID
q 不保存退出
w 保存退出
对新硬盘格式化:mkfs.ext4 /dev/sdb1(这个数字是分区时指定的,fdisk -l 中可查)
创建挂载目录:mkdir /挂载点
挂载分区:mount /dev/sdb1 /挂载点
(卸载分区:umount /dev/sdb1)
开机自动挂载:
vi /etc/fstab
--------------------------------------------------------------------------------------------
相关命令:
df -hT 查看已挂载设备的用量及类型
fdisk -l 查看分区类型等
free -m 查看内存及 swap 用量
--------------------------------------------------------------------------------------------
若在两块硬盘的电脑上重新安装系统,则默认建立 LVM 卷组,如下:
设备 大小 挂载点/RAID/卷 类型 格式
LVM 卷组
VolGroup 390540
lv_root 51200 / ext4 √
lv_home 335452 /home ext4 √
lv_swap 3888 swap √
硬盘驱动器
sda
sda1 500 /boot ext4 √
sda2 152126 VolGroup physical volume (LVM) √
sdb
sdb1 238417 VolGroup physical volume (LVM) √
总结:VolGroup 逻辑卷组的大小是 sda2 和 sdb1 大小之和。(因为 sda2 和 sdb1 的挂载点都是 VolGroup)
VolGroup 视为一个硬盘整体再分成 lv_root、lv_home、lv_swap 等分区。
交换分区也在逻辑卷内,其类型是 swap。
除了 swap 和 LVM 类型,其它分区基本是 ext4 类型了。
若在仅有一块硬盘的电脑上重新安装系统,也是按这种格局分区,
只是硬盘驱动器那只能看到一块硬盘,且 VolGroup 的大小就是那个类型为 LVM 的分区的大小。
前言
最近在学习Web Api框架的时候接触到了async/await,这个特性是.NET 4.5引入的,由于之前对于异步编程不是很了解,所以花费了一些时间学习一下相关的知识,并整理成这篇博客,如果在阅读的过程中发现不对的地方,欢迎大家指正。
同步编程与异步编程
通常情况下,我们写的C#代码就是同步的,运行在同一个线程中,从程序的第一行代码到最后一句代码顺序执行。而异步编程的核心是使用多线程,通过让不同的线程执行不同的任务,实现不同代码的并行运行。
前台线程与后台线程
关于多线程,早在.NET2.0时代,基础类库中就提供了Thread实现。默认情况下,实例化一个Thread创建的是前台线程,只要有前台线程在运行,应用程序的进程就一直处于运行状态,以控制台应用程序为例,在Main方法中实例化一个Thread,这个Main方法就会等待Thread线程执行完毕才退出。而对于后台线程,应用程序将不考虑其是否执行完毕,只要应用程序的主线程和前台线程执行完毕就可以退出,退出后所有的后台线程将被自动终止。来看代码应该更清楚一些:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("主线程开始");
//实例化Thread,默认创建前台线程
Thread t1 = new Thread(DoRun1);
t1.Start();
//可以通过修改Thread的IsBackground,将其变为后台线程
Thread t2 = new Thread(DoRun2) { IsBackground = true };
t2.Start();
Console.WriteLine("主线程结束");
}
static void DoRun1()
{
Thread.Sleep(500);
Console.WriteLine("这是前台线程调用");
}
static void DoRun2()
{
Thread.Sleep(1500);
Console.WriteLine("这是后台线程调用");
}
}
}运行上面的代码,可以看到DoRun2方法的打印信息“这是后台线程调用”将不会被显示出来,因为应用程序执行完主线程和前台线程后,就自动退出了,所有的后台线程将被自动终止。这里后台线程设置了等待1.5s,假如这个后台线程比前台线程或主线程提前执行完毕,对应的信息“这是后台线程调用”将可以被成功打印出来。
Task
.NET 4.0推出了新一代的多线程模型Task。async/await特性是与Task紧密相关的,所以在了解async/await前必须充分了解Task的使用。这里将以一个简单的Demo来看一下Task的使用,同时与Thread的创建方式做一下对比。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
namespace TestApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("主线程启动");
//.NET 4.5引入了Task.Run静态方法来启动一个线程
Task.Run(() => { Thread.Sleep(1000); Console.WriteLine("Task1启动"); });
//Task启动的是后台线程,假如要在主线程中等待后台线程执行完毕,可以调用Wait方法
Task task = Task.Run(() => { Thread.Sleep(500); Console.WriteLine("Task2启动"); });
task.Wait();
Console.WriteLine("主线程结束");
}
}
}首先,必须明确一点是Task启动的线程是后台线程,不过可以通过在Main方法中调用task.Wait()方法,使应用程序等待task执行完毕。Task与Thread的一个重要区分点是:Task底层是使用线程池的,而Thread每次实例化都会创建一个新的线程。这里可以通过这段代码做一次验证:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
namespace TestApp
{
class Program
{
static void DoRun1()
{
Console.WriteLine("Thread Id =" + Thread.CurrentThread.ManagedThreadId);
}
static void DoRun2()
{
Thread.Sleep(50);
Console.WriteLine("Task调用Thread Id =" + Thread.CurrentThread.ManagedThreadId);
}
static void Main(string[] args)
{
for (int i = 0; i < 50; i++)
{
new Thread(DoRun1).Start();
}
for (int i = 0; i < 50; i++)
{
Task.Run(() => { DoRun2(); });
}
//让应用程序不立即退出
Console.Read();
}
}
}运行代码,可以看到DoRun1()方法每次的Thread Id都是不同的,而DoRun2()方法的Thread Id是重复出现的。我们知道线程的创建和销毁是一个开销比较大的操作,Task.Run()每次执行将不会立即创建一个新线程,而是到CLR线程池查看是否有空闲的线程,有的话就取一个线程处理这个请求,处理完请求后再把线程放回线程池,这个线程也不会立即撤销,而是设置为空闲状态,可供线程池再次调度,从而减少开销。
Task<TResult>
Task<TResult>是Task的泛型版本,这两个之间的最大不同是Task<TResult>可以有一个返回值,看一下代码应该一目了然:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
namespace TestApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("主线程开始");
Task task = Task.Run(() => { Thread.Sleep(1000); return Thread.CurrentThread.ManagedThreadId.ToString(); });
Console.WriteLine(task.Result);
Console.WriteLine("主线程结束");
}
}
}Task<TResult>的实例对象有一个Result属性,当在Main方法中调用task.Result的时候,将等待task执行完毕并得到返回值,这里的效果跟调用task.Wait()是一样的,只是多了一个返回值。
async/await 特性
经过前面的铺垫,终于迎来了这篇文章的主角async/await,还是先通过代码来感受一下这两个特性的使用。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
namespace TestApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("-------主线程启动-------");
Task task = GetLengthAsync();
Console.WriteLine("Main方法做其他事情");
Console.WriteLine("Task返回的值" + task.Result);
Console.WriteLine("-------主线程结束-------");
}
static async Task GetLengthAsync()
{
Console.WriteLine("GetLengthAsync Start");
string str = await GetStringAsync();
Console.WriteLine("GetLengthAsync End");
return str.Length;
}
static Task GetStringAsync()
{
return Task.Run(() => { Thread.Sleep(2000); return "finished"; });
}
}
}首先来看一下async关键字。async用来修饰方法,表明这个方法是异步的,声明的方法的返回类型必须为:void或Task或Task<TResult>。返回类型为Task的异步方法中无需使用return返回值,而返回类型为Task<TResult>的异步方法中必须使用return返回一个TResult的值,如上述Demo中的异步方法返回一个int。而返回类型可为void,则是为了和事件处理程序兼容,比如下面的示例:
public Form1()
{
InitializeComponent();
btnDo.Click += Down;
}
public async void Down(object sender, EventArgs e)
{
btnDo.Enabled = false;
string str = await Run();
labText.Text = str;
btnDo.Enabled = true;
}
public Task Run()
{
return Task.Run(() => { Thread.Sleep(5000); return DateTime.Now.ToString(); });
}再来看一下await关键字。await必须用来修饰Task或Task<TResult>,而且只能出现在已经用async关键字修饰的异步方法中。
通常情况下,async/await必须成对出现才有意义,假如一个方法声明为async,但却没有使用await关键字,则这个方法在执行的时候就被当作同步方法,这时编译器也会抛出警告提示async修饰的方法中没有使用await,将被作为同步方法使用。了解了关键字async\await的特点后,我们来看一下上述Demo在控制台会输入什么吧。

输出的结果已经很明确地告诉我们整个执行流程了。GetLengthAsync异步方法刚开始是同步执行的,所以"GetLengthAsync Start"字符串会被打印出来,直到遇到第一个await关键字,真正的异步任务GetStringAsync开始执行,await相当于起到一个标记/唤醒点的作用,同时将控制权放回给Main方法,"Main方法做其他事情"字符串会被打印出来。之后由于Main方法需要访问到task.Result,所以就会等待异步方法GetLengthAsync的执行,而GetLengthAsync又等待GetStringAsync的执行,一旦GetStringAsync执行完毕,就会回到await GetStringAsync这个点上执行往下执行,这时"GetLengthAsync End"字符串就会被打印出来。
当然,我们也可以使用下面的方法完成上面控制台的输出。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
namespace TestApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("-------主线程启动-------");
Task task = GetLengthAsync();
Console.WriteLine("Main方法做其他事情");
Console.WriteLine("Task返回的值" + task.Result);
Console.WriteLine("-------主线程结束-------");
}
static Task GetLengthAsync()
{
Console.WriteLine("GetLengthAsync Start");
Task task = Task.Run(() => { string str = GetStringAsync().Result;
Console.WriteLine("GetLengthAsync End");
return str.Length; });
return task;
}
static Task GetStringAsync()
{
return Task.Run(() => { Thread.Sleep(2000); return "finished"; });
}
}
}对比两种方法,是不是async\await关键字的原理其实就是通过使用一个线程完成异步调用吗?答案是否定的。async关键字表明可以在方法内部使用await关键字,方法在执行到await前都是同步执行的,运行到await处就会挂起,并返回到Main方法中,直到await标记的Task执行完毕,才唤醒回到await点上,继续向下执行。更深入点的介绍可以查看文章末尾的参考文献。
async/await 实际应用
微软已经对一些基础类库的方法提供了异步实现,接下来将实现一个例子来介绍一下async/await的实际应用。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
namespace TestApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("开始获取博客园首页字符数量");
Task task1 = CountCharsAsync("http://www.cnblogs.com");
Console.WriteLine("开始获取百度首页字符数量");
Task task2 = CountCharsAsync("http://www.baidu.com");
Console.WriteLine("Main方法中做其他事情");
Console.WriteLine("博客园:" + task1.Result);
Console.WriteLine("百度:" + task2.Result);
}
static async Task CountCharsAsync(string url)
{
WebClient wc = new WebClient();
string result = await wc.DownloadStringTaskAsync(new Uri(url));
return result.Length;
}
}
}参考文献:<IIIustrated C# 2012> 关于async/await的FAQ 《深入理解C#》