1. 简单的委托异步调用
看一个非常简单的C#委托异步调用:
static void Main()
{
//定义委托
var del = new Func<string, char, int>(doo);
//调用BeginInvoke
del.BeginInvoke("a", 'b', callback, del);
Console.ReadKey();
}
static void callback(IAsyncResult ar)
{
//从AsyncState中提取委托
var del = (Func<string, char, int>)ar.AsyncState;
Console.WriteLine("callback调用");
//调用EndInvoke
var res = del.EndInvoke(ar);
Console.WriteLine("doo返回值:{0}", res);
}
//异步执行方法
static int doo(string a, char b)
{
Console.WriteLine("doo调用:{0} {1}", a, b);
return 1;
}
上述代码会输出:
doo调用:a b
doo返回值:1
doo被异步调用,同时EndInvoke方法被调用后,其返回值被输出。
上面的代码存在异步调用中常见的一些繁琐问题:
- 必须把委托作为参数传入到回调方法中(当然使用Lambda或者匿名委托捕获外部变量可以避免)
- 必须在回调方法中调用委托的EndInvoke,并且传入IAsyncResult参数从而使异步调用完成。
.NET 4.0 TPL中的TaskFactory.FromAsync可以很大程度上简化异步操作,于是上面的代码简化成这样:
static void Main()
{
var del = new Func<string, char, int>(doo);
Task<int>.Factory.FromAsync(del.BeginInvoke, del.EndInvoke, "a", 'b', null)
.ContinueWith(t => Console.WriteLine("doo返回值:{0}", t.Result));
Console.ReadKey();
}
//异步执行方法
static int doo(string a, char b)
{
Console.WriteLine("doo调用:{0} {1}", a, b);
return 1;
}
注意FromAsync是泛型方法,会根据BeginInvoke委托来匹配后面的参数类型(事实上这是Visual Studio的功劳),那么实际上面的FromAsync完整泛型调用是这样的:
Task<int>.Factory.FromAsync<string, char>(del.BeginInvoke, del.EndInvoke, "a", 'b', null)
.ContinueWith(t => Console.WriteLine("doo返回值:{0}", t.Result));
注意此时不需要传入异步调用的而外参数,所以FromAsync的state参数总是为null。
返回目录2. 带有异常的委托异步调用
对于带有异常的委托异步调用,同样,我们必须调用委托对象的EndInvoke来捕获异常,如下代码:
static void Main()
{
var del = new Action(doo);
del.BeginInvoke(callback, del);
Console.ReadKey();
}
//回调方法
static void callback(IAsyncResult ar)
{
Console.WriteLine("callback调用");
var del = (Action)ar.AsyncState;
try
{
del.EndInvoke(ar);
}
catch (Exception ex)
{
Console.WriteLine("捕获doo异常:{0}", ex.Message);
}
}
//异步执行方法
static void doo()
{
Console.WriteLine("doo调用");
throw new Exception("doo错误");
}
代码会输出:
doo调用
callback调用
捕获doo异常:doo错误
现在我们一TPL的方式简化操作:
static void Main()
{
var del = new Action(doo);
Task.Factory.FromAsync(del.BeginInvoke, del.EndInvoke, null);
Console.ReadKey();
}
//异步执行方法
static void doo()
{
Console.WriteLine("doo调用");
throw new Exception("doo错误");
}
输出很有意思,竟然是:
doo调用
没有异常抛出,但是doo方法又确实运行了!原因则是Task运行对异常处理的特殊机制,读者可以参考我的另一篇文章:
.NET(C#) TPL:Task中未觉察异常和TaskScheduler.UnobservedTaskException事件
这个异常可以通过强行垃圾回收,此时这个未觉察状态的异常会在垃圾回收时终结器执行线程中被抛出。如下代码(修改Main方法如下,其他代码不变):
var del = new Action(doo);
Task.Factory.FromAsync(del.BeginInvoke, del.EndInvoke, null);
//确保任务完成
Thread.Sleep(1000);
//强制垃圾会受到
GC.Collect();
//等待终结器处理
GC.WaitForPendingFinalizers();
输出:
doo调用
Unhandled Exception: System.AggregateException: A Task's exception(s) were not o
bserved either by Waiting on the Task or accessing its Exception property. As a
result, the unobserved exception was rethrown by the finalizer thread. ---> Syst
em.Exception: doo错误
Server stack trace:
at Mgen.Program.doo() in E:\Users\Mgen\Documents\Visual Studio 2010\Projects\
ConsoleApplication1\ConsoleApplication1\Program.cs:line 34
...(省略)
这个异常会被抛出。
当然根据本例,如果想捕获异常,可以使用Task.Wait来等待Task结束并通过AggregateException.InnerExceptions接收异常:
var del = new Action(doo);
var task = Task.Factory.FromAsync(del.BeginInvoke, del.EndInvoke, null);
try
{
task.Wait();
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
Console.WriteLine("doo方法异常:{0}", ex.Message);
}
不过上面的代码我的方法调用就不是异步的了,因此更好的方法还是使用ContinueWith,等Task结束后,通过Task.Exception(同样返回AggregateException对象)来枚举异常,如下代码:
var del = new Action(doo);
Task.Factory.FromAsync(del.BeginInvoke, del.EndInvoke, null)
.ContinueWith(t =>
{
foreach(var ex in t.Exception.InnerExceptions)
Console.WriteLine("doo方法异常:{0}", ex.Message);
});
Console.ReadKey();
输出正确:
doo调用
doo方法异常:doo错误
上面都是讲怎样捕获异常,如果你想忽略这个异常(同样不想让这个异常在垃圾回收时在终结器执行线程中被抛出)可以简单的引用一下Exception属性,TaskContinuationOptions.OnlyOnFaulted来使引用Exception属性只发生在发生异常时(即Exception为null的时候没必要再去引用它)。就像这样:
var del = new Action(doo);
Task.Factory.FromAsync(del.BeginInvoke, del.EndInvoke, null)
.ContinueWith(t => { var exp = t.Exception; }, TaskContinuationOptions.OnlyOnFaulted);
Console.ReadKey();
3. 有ref或out的委托异步调用
对于此类委托异步调用,由于委托泛型定义不支持ref和out参数,所以此类委托异步调用无法用FromAsync完成,只能用原始委托异步调用来做。
代码:
delegate void MyDelegate(string a, out int b);
static void Main()
{
var del = new MyDelegate(doo);
int b;
var ar = del.BeginInvoke("a", out b, callback, del);
Console.ReadKey();
}
static void callback(IAsyncResult ar)
{
var del = (MyDelegate)ar.AsyncState;
Console.WriteLine("callback调用");
int res;
del.EndInvoke(out res, ar);
Console.WriteLine("doo out参数值:{0}", res);
}
输出:
doo调用
callback调用
doo out参数值:71
4. .NET Framework中预定义的异步调用
.NET Framework中也定义了许多类似委托异步调用的方法,其实此类异步调用又称APM:异步编程模式(全称Asynchronous Programming Model),此类异步方法的命名以Beginxxx和Endxxx。TaskFactory.FromAsync处理.NET Framework中预定义的异步调用同样游刃有余。
原因是TaskFactory.FromAsync定义了诸多重载可以满足多数预定义的异步调用,比如下面是一个比较长的重载:
//TaskFactory.FromAsync的重载之一
public Task<TResult> FromAsync<TArg1, TArg2, TArg3>(Func<TArg1, TArg2, TArg3, AsyncCallback, object, IAsyncResult> beginMethod,Func<IAsyncResult, TResult> endMethod, TArg1 arg1, TArg2 arg2, TArg3 arg3, object state, TaskCreationOptions creationOptions);
我们就以Stream类型的异步读取做例子,不用FromAsync的话这样写:
var ms = new MemoryStream(new byte[] { 1, 2, 3 });
byte[] data = new byte[10];
ms.BeginRead(data, 0, data.Length, ir =>
{
var read = ms.EndRead(ir);
Console.WriteLine(BitConverter.ToString(data.Take(read).ToArray()));
}, null);
Console.ReadKey();
而使用FromAsync:
var ms = new MemoryStream(new byte[] { 1, 2, 3 });
byte[] data = new byte[10];
var task = Task<int>.Factory.FromAsync(ms.BeginRead, ms.EndRead, data, 0, data.Length, null)
.ContinueWith(t => Console.WriteLine(BitConverter.ToString(data.Take(t.Result).ToArray())));
Console.ReadKey();
后者更精短。
:D
本文版权归作者所有,欢迎以网址(链接)的方式转载,不欢迎复制文章内容的方式转载,其一是为了在搜索引擎中去掉重复文章内容,其二复制后的文章往往没有提供本博客的页面格式和链接,造成文章可读性很差。望有素质人自觉遵守上述建议。
如果一定要以复制文章内容的方式转载,必须在文章开头标明作者信息和原文章链接地址。否则保留追究法律责任的权利。