.net 异步
原文:https://www.cnblogs.com/wisdomqq/archive/2012/03/26/2412349.html
在说到异步前,先来理一下几个容易混淆的概念,并行、多线程、异步。
并行,一般指并行计算,是说同一时刻有多条指令同时被执行,这些指令可能执行于同一CPU的多核上,或者多个CPU上,或者多个物理主机甚至多个网络中。
多线程,一般指同一进程中多个线程(包含其数据结构、上下文与代码片段)协作运行。在多核计算机中多个线程将有机会同时运行于多个核上,如果线程中进行的是计算,则行成并行计算。
异步,与同步相对应,是指呼叫另一操作后,不等待其结果,继续执行之后的操作,若之后没有其他操作,当前线程将进入睡眠状态,而CPU时间将有机会切至其他线程。在异步操作完成后通过回调函数的方式获取通知与结果。异步的实现方式有多种,如多线程与完成端口。多线程将异步操作放入另一线程中运行,通过轮询或回调方法得到完成通知;完成端口,由操作系统接管异步操作的调度,通过硬件中断,在完成时触发回调方法,此方式不需要占用额外线程。
本文讨论.NET下的异步,以及其进化过程中出现的多种异步模式。
首先看一下两段需要花较长时间运行的代码在同步方式下的情形。
public class ProgramClass
{
public static void Main()
{
using (var fs = new FileStream("Data.dat", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096))
{
fs.Write(new byte[100], 0, 100);
} DoSomething(); Console.WriteLine("END");
} static string DoSomething()
{
Thread.Sleep(2000);
return "Finished";
}
}
同步方式运行时,所有操作会顺序执行,当某方法被阻塞时,线程即进入阻塞状态。该情形下,CPU时间无法得到充分利用,当前线程长时间处于阻塞状态,任务总时间长。
开始异步化
为提高CPU使用率,从而减少任务时间,采用多线程方式实现异步调用。
public class ProgramClass
{
public static void Main()
{
Thread writeThread = new Thread(new ThreadStart(WriteWapper));
Thread doSomethingThread = new Thread(new ParameterizedThreadStart(DoSomethingWapper)); ClosureClass closure = new ClosureClass();
writeThread.Start();
doSomethingThread.Start(closure);//闭包对象,用于变量穿越 writeThread.Join();
doSomethingThread.Join(); Console.WriteLine(closure.Result);
} //将方法包装成适于线程调用的签名
private static void WriteWapper()
{
using (var fs = new FileStream("Data.dat", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096))
{
fs.Write(new byte[100], 0, 100);
}
} //将方法包装成适于线程调用的签名
static void DoSomethingWapper(object state)
{
ClosureClass closure = state as ClosureClass;
var result = DoSomething();
if (closure != null)
{
closure.Result = result;
}
} static string DoSomething()
{
Thread.Sleep(2000);
return "Finished";
} //闭包辅助类,用于存储在方法间传递内部变量与参数
class ClosureClass
{
//存储方法返回值
public string Result { get; set; }
}
}
利用多线程将耗时操作放入其他线程中进行处理,主线程继续做自己的事(本例中,主线程进行等待其他线程完成)。从而减少任务处理时间。
【注意】本例中,write与dosomething操作内部均有线程等待,在单核中依然可以通过操作系统的线程切换提高CPU使用率,但是如果操作是需要大量CPU计算,则在单核情况下并不一定能够提高CPU使用率,并且可能增加线程调试的开销,因此单核情况下此种方式不适合用于密集型运算。
【提示】对于线程的入口方法,我们往往会对其进行包装,形成一致的方法签名、处理异常、拦截请求等。在本例中,由于被调用的方法有输入与输出,困此采用辅助对象进行传递,在C#2开始引入的闭包,采用类似的原理实现,从而减少大量的代码,并提高程序可读性。
public class ProgramClass
{
public static void Main()
{
string result = null; Thread writeThread = new Thread(new ThreadStart(WriteWapper));
Thread doSomethingThread = new Thread(new ThreadStart(() =>
{
result = DoSomething();//跨方法访问临时变量,形成闭包
})); writeThread.Start();
doSomethingThread.Start(); writeThread.Join();
doSomethingThread.Join(); Console.WriteLine(result);
} //将方法包装成适于线程调用的签名
private static void WriteWapper()
{
using (var fs = new FileStream("Data.dat", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096))
{
fs.Write(new byte[100], 0, 100);
}
} static string DoSomething()
{
Thread.Sleep(2000);
return "Finished";
}
}
开启一个新线程将带来可观的开销,因此我们希望能够重用线程,在.NET中,可以采用线程池达到这一目的,同时简化线程的操作。
public class ProgramClass
{
public static void Main()
{
string result = null; AutoResetEvent resetEvent = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem(new WaitCallback(state =>
{
result = DoSomething();
resetEvent.Set();
})); resetEvent.WaitOne();
Console.WriteLine(result);
} static string DoSomething()
{
Thread.Sleep(2000);
return "Finished";
}
}
由于线程池中,我们无法对线程进行更为细致的操作,为得到操作完成的通知,我们需要在包装方法中,在操作完成后加入适当的代码,本例中我们采用ResetEvent进行线程的同步。
【注意】在ASP.NET中,所有的WEB线程均运行于线程池,因此线程池中的线程是非常宝贵的资源,耗尽线程池中的线程将可能引起所有的请求进入等待队列,从而无法提供服务,在ASP.NET中的线程池操作应该更为谨慎。
完成端口与异步模型
到这里为止,都是采用多线程的方式手动实现了异步,正如前面所说,多线程不适用于单核密集运算,在非密集运算下也会产生线程调度的开销,在需要大量线程的应用中会浪费宝贵资源。考察需要阻塞等待的场景,往往是与系统外部数据交换有关,如大量内存数据的复制、读写磁盘文件、访问网络等,这种情况下,在硬件完成操作前CPU无能为力,因此只能等待,更完美的方案是发出指令后不进入等待,当操作完毕后通过某种方式得到通知并执行相关代码,称为完成端口。
完成端口编程复杂,并且需要操作系统支持,使用中需要先判断是否支持,再采用不同的方式去实现,并且实现的方法多样,在异步使用频繁的今天,为简化异步操作,往往会制订一种统一的异步模型,并且这类模型也在不断进化中。
在介绍异步模型时,我们会用不同的方法先将一个普通方法异步调用,再调用类库中提供的异步方法,然后实现一个自己的异步方法,最后将多个异步方法按顺序调用包装成新的异步方法。
在早期的.NET中,采用 BeginXXX/EndXXX 方式实现异步。
对于普通的方法,可以采用委托的 BeginInvoke / EndInvoke 实现异步化。
public class ProgramClass
{
public static void Main()
{
string result = null; var doSomgthingDelegate = new Func<string>(DoSomething);
var asyncResult = doSomgthingDelegate.BeginInvoke(new AsyncCallback(aresult =>
{
result = doSomgthingDelegate.EndInvoke(aresult);
}), null); asyncResult.AsyncWaitHandle.WaitOne(); Console.WriteLine(result);
} static string DoSomething()
{
Thread.Sleep(2000);
return "Finished";
}
}
委托的异步内部采用线程池实现。
有些类库中的方法,实现了异步版本。
public static void Main()
{
using (var fs = new FileStream("Data.dat", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096
, FileOptions.Asynchronous))
{
fs.Write(new byte[100], 0, 100);
var asyncResult = fs.BeginWrite(new byte[100], 0, 100, new AsyncCallback(aresult => {
fs.EndWrite(aresult);//执行完毕后的回调方法
}), null); asyncResult.AsyncWaitHandle.WaitOne();
}
}
对于类库的方法的异步版本,内部会进行判断决定采用何种方式实现。
【注意】对于FileStream,必须加上FileOptions.Asynchronous才会有机会使用完成端口。
现在我们可以根据这个模型来实现自己的异步方法。
public class ProgramClass
{
public static void Main()
{
DoSomeThing(); var result = BeginDoSomeThing(1, new AsyncCallback(aresult =>
{
ProgramClass.EndDoSomeThing(aresult);
}), null);
result.AsyncWaitHandle.WaitOne();
} //同步版本
public static string DoSomeThing()
{
Thread.Sleep(2000);
return "Finished";
} //异步版本开始
public static IAsyncResult BeginDoSomeThing(int arg1, AsyncCallback callback, object state)
{
var asyncResult = new DoSomethingAsyncResult(callback, state); Timer timer = null;
timer = new Timer(new TimerCallback(s =>
{
timer.Dispose();
asyncResult.SetComplete("Finished");
}), state, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2)); return asyncResult;
} //异步版本结束
public static string EndDoSomeThing(IAsyncResult asyncResult)
{
DoSomethingAsyncResult result = asyncResult as DoSomethingAsyncResult;
if (result != null)
{
return result.Result;
}
return null;
} //AsyncResult对象
public class DoSomethingAsyncResult : IAsyncResult
{
private AsyncCallback _asyncCallback;
private AutoResetEvent _asyncWaitHandle; public DoSomethingAsyncResult(AsyncCallback asyncCallback, object state)
{
AsyncState = state;
_asyncCallback = asyncCallback;
_asyncWaitHandle = new AutoResetEvent(false);
} //设置结果
public void SetComplete(string result)
{
Result = result;
IsCompleted = true;
if (_asyncCallback != null)
{
_asyncCallback(this);
}
_asyncWaitHandle.Set();
} public string Result
{
get;
private set;
} public object AsyncState
{
get;
private set;
} public WaitHandle AsyncWaitHandle
{
get { return _asyncWaitHandle; }
} public bool CompletedSynchronously
{
get { return false; }
} public bool IsCompleted
{
get;
private set;
}
}
}
本例中,采用定时器触发完成动作,实际中,可以在需要的时候触发完成。
对于BeginXXX/EndXXX模式,调用BeginXXX表示开始一个异步方法,前面的参数表示方法所需的参数(可无),倒数第二个参数为回调方法(可空),最后一个参数用于穿越整个过程的相关对象(可空)。返回的IAsyncResult存储了异步方法的相关状态信息,一般来说我们自己的异步方法需要一个实现了该接口的类,类中包含了回调方法、等待对象、相关参数与结果等。
异步方法的协作有三种方法,第一种,通过轮询 IsCompleted 属性,直到为true时,触发完成动作。
第二种,通过回调方法,当异步方法完成时,由异步方法调用回调方法。
第三种,通过WaitHandler等待异步方法完成,当异步方法完成时,由异步方法发出完成信号,使等待结束。
当我们需要将多个异步方法包装成一个异步方法时,方法内部将充斥着大量的回调方法。
类似于这样:
public static IAsyncResult BeginDoSomeThing(int arg1, AsyncCallback callback, object state)
{
var asyncResult = new DoSomethingAsyncResult(callback, state); Timer timer = null;
timer = new Timer(new TimerCallback(s =>
{
timer.Dispose(); using (var fs = new FileStream("Data.dat", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous))
{
var writeresult = fs.BeginWrite(new byte[100], 0, 100, new AsyncCallback(wresult =>
{
fs.EndWrite(wresult); asyncResult.SetComplete("Finished");
}), null);
}
}), state, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2)); return asyncResult;
}
呼~幸好还有匿名方法与闭包,否则将是一件多么恐怖的事啊。
新的异步模型
我们清醒的看到,当需要多个异步方法需要协作时,代码将显得十分复杂,无法表现清晰的逻辑,于是,我们需要一个更好的异步模型。
从.NET4开始,引入了新的异步模型。
首先引入一个新概念:Task。
Task代表一个可以被执行的任务,我们可以让他运行,关联其他任务,等待他,获取他的结果。值得注意的是,这里的Task可以是一个异步的任务,也可以是同步的任务,在没有特别说明的情况下都指异步任务。而返回一个Task对象的方法,我们一般认为这是一个异步方法。new Task或者Task.Run将生成一个在线程池中运行的异步任务。
的按照惯例,我们看一下如何把一个普通的方法异步执行。
public static void Main()
{
var t1 = Task<int>.Run(() =>
{
Thread.Sleep(2000);
return 100;
}).ContinueWith(new Action<Task<int>>(t =>
{
Console.WriteLine(t.Result);
}));
t1.Wait();
}
对于类库中提供的异步方法,也有了新版本,XXXAsync。
public static void Main()
{
using (var fs = new FileStream("Data.dat", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096,
FileOptions.Asynchronous))
{
var task = fs.WriteAsync(new byte[100], 0, 100)
.ContinueWith(new Action<Task>(t => {
Console.WriteLine("Finished");
}));
task.Wait();
}
}
我们不再关心如何去开始,何时会结束,一切变成了一些有关或无关的任务。
让我们自己写一个异步方法吧。
public static Task<string> DoSomethingAsync(int value)
{
return Task<string>.Run(() =>
{
Thread.Sleep(2000);
return value.ToString();
}); ;
}
好吧,你肯定是以我在偷懒,为什么不像BeginXXX/EndXXX一样从底层开始实现一个呢,那是因为Task的封装比较严,我们无法直接对其扩展。为了达到获取一个Task,在需要的时候设置完成与结果,可以借助 AsyncTaskMethodBuilder 来实现。
public class ProgramClass
{
public static void Main()
{
var task = ProcessAsync();
task.Wait();
var r = task.Result;
} static Task<string> ProcessAsync()
{
//辅助工具
AsyncTaskMethodBuilder<string> builder = AsyncTaskMethodBuilder<string>.Create();
Timer timer = null;
timer = new Timer(s =>
{
timer.Dispose();
builder.SetResult("Finished");//在需要时设置结果
}, null, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2));
return builder.Task;//获取需要的Task
}
}
类似的方法,我们封装一个由多个异步方法组合成的异步方法。
public class ProgramClass
{
public static void Main()
{
var task = ProcessAsync();
task.Wait();
var r = task.Result;
} static Task<string> ProcessAsync()
{
//辅助工具
AsyncTaskMethodBuilder<string> builder = AsyncTaskMethodBuilder<string>.Create(); DoSomethingAync1().GetAwaiter().OnCompleted(() =>
{
DoSomethingAync2().GetAwaiter().OnCompleted(() =>
{
DoSomethingAync2().GetAwaiter().OnCompleted(() =>
{
builder.SetResult("Finished");
});
});
}); return builder.Task;//获取需要的Task
} static Task<string> DoSomethingAync1() { ... }
static Task<string> DoSomethingAync2() { ... }
static Task<string> DoSomethingAync3() { ... }
}
组合异步方法调用后,按顺序调用第一个异步方法,紧接着,产生需要的结果Task后返回。异步方法完成时回调指定的方法,并按顺序继续调用,所有方法完成后,把运行的最终结果设置给结果Task,那么整个任务即完成。
如果异步方法有回返值,那么组合的异步方法看上去会复杂一点。
static Task<string> ProcessAsync()
{
//辅助工具
AsyncTaskMethodBuilder<string> builder = AsyncTaskMethodBuilder<string>.Create(); string r1, r2, r3;//用于存储每一个任务的结构
var awaitor1 = DoSomethingAync1().GetAwaiter();
awaitor1.OnCompleted(() =>
{
r1 = awaitor1.GetResult();
var awaitor2 = DoSomethingAync2().GetAwaiter();
awaitor2.OnCompleted(() =>
{
r2 = awaitor2.GetResult();
var awaitor3 = DoSomethingAync3().GetAwaiter();
awaitor3.OnCompleted(() =>
{
r3 = awaitor3.GetResult();
builder.SetResult(r1 + r2 + r3);//计算最终结构并设置结果
});
});
}); return builder.Task;//获取需要的Task
}
代码虽然复杂了一点,但还能够接受,这里的每个异步方法的返回值需要临时变量来存储,包括每个异步方法的TaskAwaiter对象,需要跨越多个方法,这里将形成闭包,使得这些对象无法尽快释放,同时,每一个异步方法都将附加一个OnComplete回访方法的委托对象,这些都是使用上述方法的代价,这些代价在理论上是可以被优化的,但是带来的是更为复杂的代码结果,暂且放下吧,因为,解决方案就在后面。
重口味语法糖
在C#5中,添加了 async/await 关键字,使得上面遗留的问题得以解决,而且重点是,用起来非常简单!
上面的代码在C#5时代可以写成下面的样子:
static async Task<string> ProcessAsync()
{
var r1 = await DoSomethingAync1();
var r2 = await DoSomethingAync2();
var r3 = await DoSomethingAync3(); return r1 + r2 + r3;
}
是不是震惊了。
他几乎和同步方法写法一致。程序的逻辑完全没有因为异步而打乱,并且减少了代码量,这就是语法糖的魅力。
语法糖的背后隐藏了不为人知的内部实现,特别重口味语法糖,我们需要知道他背后的实现,才不致于消化不良。
先看一下语法,async关键字告诉编译器,对本方法使用语法糖,对于这类方法只能返回 void/Task/Task<T>,返回void/Task代表异步方法不返回任何结果,返回void在调用方看来是一个同步方法,没有机会获取异步回调。返回Task<T>代表返回结果为T的异步方法,在该方法内部,可以直接返回类型为T的结果。在返回结果前可以使用await关键字调用其他异步方法,并且可以直接获取该异步方法的返回值。无需处理任何Task相关的内容。当async方法内部没有任何await时,该方法效果与同步相同,仅仅是简单包装后Task而已。
该方法的执行顺序与前面我们自己实现的相同,内部实现也有一些类似,同样采用AsyncTaskBuilder构建Task对象,在我们自己实现的方法中,在方法内部(一个或多个匿名方法与闭包对象)实现多个异步方法的调度,而async/await语法糖则采用一个状态机对象作为媒介进行多个异步方法的调度。
编译后,async异步方法将执行过程委托给状态机,自己则向AsyncTaskBuilder获取Task返回,状态机内部存储方法内部参与计算的临时变量(闭包),维护当前执行状态,-1代表开始与中间状态,-2代表结束,0-n代表正在执行第n个异步方法,状态机的MoveNext方法按顺序去调用其他的异步方法,如异步方法已执行完毕则继续往下执行,如未完毕,则设置当前状态,存储任务的Awaiter对象,并关联完成动作(状态机方法本身的单例委托对象)后结束,当异步方法执行完毕,继续调用状态机MoveNext方法,按照状态找到执行入口点,找到上次执行的Awaiter对象,并获取执行结果,然后继续找到下一个异步方法执行,重复以上的步骤,如果异步方法间有其他代码,照本执行,当所有异步方法与内部代码执行完毕后,通过AsyncTaskBuilder向异步方法的结果Task设置结果值,该Task即完成。
从编译后的结果可以看到,这里不再存在闭包对象与多个回调方法及其委托对象,全部合在状态机对象当中,而每一次异步方法调用后的Awaiter对象也可以在异步方法完成后释放引用,在状态机对象中根据签名的种类提供必要的字段位置,状态机本身也是结构体,最大限度上减少了空间的开销与GC的压力,而所有的这一切,编译器通通搞定,而程序员,只需要关注逻辑的顺序与结果的处理即可。
.net 异步的更多相关文章
- 异步任务队列Celery在Django中的使用
前段时间在Django Web平台开发中,碰到一些请求执行的任务时间较长(几分钟),为了加快用户的响应时间,因此决定采用异步任务的方式在后台执行这些任务.在同事的指引下接触了Celery这个异步任务队 ...
- C#异步编程(一)
异步编程简介 前言 本人学习.Net两年有余,是第一次写博客,虽然写的很认真,当毕竟是第一次,肯定会有很多不足之处, 希望大家照顾照顾新人,有错误之处可以指出来,我会虚心接受的. 何谓异步 与同步相对 ...
- redux-amrc:用更少的代码发起异步 action
很多人说 Redux 代码多,开发效率低.其实 Redux 是可以灵活使用以及拓展的,经过充分定制的 Redux 其实写不了几行代码.今天先介绍一个很好用的 Redux 拓展-- redux-amrc ...
- C#与C++的发展历程第三 - C#5.0异步编程巅峰
系列文章目录 1. C#与C++的发展历程第一 - 由C#3.0起 2. C#与C++的发展历程第二 - C#4.0再接再厉 3. C#与C++的发展历程第三 - C#5.0异步编程的巅峰 C#5.0 ...
- 关于如何提高Web服务端并发效率的异步编程技术
最近我研究技术的一个重点是java的多线程开发,在我早期学习java的时候,很多书上把java的多线程开发标榜为简单易用,这个简单易用是以C语言作为参照的,不过我也没有使用过C语言开发过多线程,我只知 ...
- 异步编程 In .NET
概述 在之前写的一篇关于async和await的前世今生的文章之后,大家似乎在async和await提高网站处理能力方面还有一些疑问,博客园本身也做了不少的尝试.今天我们再来回答一下这个问题,同时我们 ...
- ajax异步请求
做前端开发的朋友对于ajax异步更新一定印象深刻,作为刚入坑的小白,今天就和大家一起聊聊关于ajax异步请求的那点事.既然是ajax就少不了jQuery的知识,推荐大家访问www.w3school.c ...
- 探索ASP.NET MVC5系列之~~~2.视图篇(上)---包含XSS防御和异步分部视图的处理
其实任何资料里面的任何知识点都无所谓,都是不重要的,重要的是学习方法,自行摸索的过程(不妥之处欢迎指正) 汇总:http://www.cnblogs.com/dunitian/p/4822808.ht ...
- C#异步编程(二)
async和await结构 序 前篇博客异步编程系列(一) 已经介绍了何谓异步编程,这篇主要介绍怎么实现异步编程,主要通过C#5.0引入的async/await来实现. BeginInvoke和End ...
- [.NET] 利用 async & await 的异步编程
利用 async & await 的异步编程 [博主]反骨仔 [出处]http://www.cnblogs.com/liqingwen/p/5922573.html 目录 异步编程的简介 异 ...
随机推荐
- 380-Xilinx Kintex UltraScale FPGA KCU1500 Acceleration Development Kit
Xilinx Kintex UltraScale FPGA KCU1500 Acceleration Development Kit Product Description The Kintex® U ...
- c++ sizeof的实现
c++中的sizeof,可以通过以下宏定义实现. #include <stdio.h> #define sizeof_T(T) ((size_t)((T*)0+1)) ///求类型的大小 ...
- Linux性能优化从入门到实战:03 CPU篇:CPU上下文切换
linux操作系统是将CPU轮流分配给任务,分时执行的.而每次执行任务时,CPU需要知道CPU寄存器(CPU内置的内存)和程序计数器PC(CPU正在执行指令和下一条指令的位置)值,这些值是CPU执 ...
- vue+element ui 滚动加载
<div id="app"> <div class="infinite-list-wrapper" style="overflow: ...
- glob & fnmatch -- 使用Unix style通配符
通配符: ? 匹配单个字符 * 匹配 0+ 个字符 [seq] 匹配属于区间的单个字符 [!seq] 匹配不属于区间的单个字符 注意: "." just a " ...
- alert(1) to win
一. function escape(s) { return '<script>console.log("'+s+'");</script>'; } 两种思 ...
- [php] phar
build.php打包www目录: <?php class A{ public $a = 1; } $p = new Phar('test.phar',0,'test.phar'); $p-&g ...
- php 输出缓冲
<?php ob_start();//开启php输出缓冲区 echo "A"; //"A"会进入php输出缓冲区 ob_flush();//将php输出缓 ...
- python数据分析第二版:pandas
一:pandas 两种数据结构:series和dataframe series:索引(索引自动生成)和标签(人为定义)组成---返回一个对象 obj = pd.Series([1,2,3,4]) ob ...
- java int整数相乘溢出
int整数相乘溢出 我们计算一天中的微秒数: * * * * ;// 正确结果应为:86400000000 System.out.println(microsPerDay);// 实际上为:50065 ...