开篇

异步编程是程序设计的重点也是难点,还记得在刚开始接触.net的时候,看的是一本c#的Winform实例教程,上面大部分都是教我们如何使用Winform的控件以及操作数据库的实例,那时候做的基本都是数据库的demo,数据量也不大,程序在执行的时候基本上不会出现阻塞的情况。随着不断的深入.net,也开始进入的实战,在实际的项目,数据量往往都是比较大,特别是在大量的数据入库以及查询数据并进行计算的时候,程序的UI界面往往卡死在那里,发生了阻塞,这时候就需要对计算时间限制的过程进行异步处理,让UI线程继续相应用户的操作,使得用户体验表现比较友好,同时正确的使用异步编程去处理计算限制的操作和耗时IO操作还能提升的应用程序的吞吐量及性能。由此可见,异步编程的重要性。

 
异步编程在程序设计中也是非常复杂的,稍有不慎,就会使得你的应用程序变得不稳定,出现异常,甚至会奔溃。但是,比较幸运的是,.net提供非常方便的框架来进行异步编程,在我看来.net中实现异步有两种方式,第一种是多线程的方式,第二种是使用异步函数,其实在异步函数中使用的还是多线程的技术。接下来就介绍在.net中如何使用多线程和异步函数来解决计算限制、耗时等这些不友好用户体验的问题。
 
异步编程中比较关心,也是比较重要的技术点在于,1)当异步线程在工作完成时如何通知调用线程,2)当异步线程出现异常的时候该如何处理,3)异步线程工作的进度如何实时的通知调用线程。4)如何在调用线程中取消正在工作的异步线程,并进行回滚操作。
 
一、异步函数模型
c#中提供异步函数编程模式,只要是使用委托对象封装的函数都可以实现该函数的异步调用,这是因为委托类型有BeginInvokeEndInvoke这两个方法来支持异步调用。
下面给出一个例子来讲解如何使用委托的来实现异步调用函数。
class Program
{
public delegate void DoWork();
static void Main(string[] args)
{
DoWork d = new DoWork(WorkPro);//no.1 d.BeginInvoke(null, null);//no.2
for (int i = ; i < ; i++)//no.3
{
Thread.Sleep();//主线程需要做的事
}
Console.WriteLine("主线程done");
Console.ReadKey();
}
public static void WorkPro()
{
//做一些耗时的工作
Thread.Sleep();
Console.WriteLine("异步调用结束");
}
}
程序定义了一个DoWork类型无参无返回值的的委托类型,no.1用WorkPro方法实例化一个DoWork类型的对象d ,no.2通过委托对象dBeginInvoke(null,null)(下面将会详细介绍BeginInvoke函数中两个参数如何使用)来实现WorkPro函数的异步调用,这样就使得no.3主线程所做的for循环和WorkPro函数可以同时执行,这样使得程序的运行效率得到了大幅度的提升。如果程序是同步执行的话,假设WorkPro函数执行需要2秒,for需要1秒,总共执行时间就需要3秒,如果WorkPro是异步执行的话,那么整个程序执行完毕只需要2秒就够了。
------
上面这个例子只是简单演示了如何通过委托来实现函数的异步调用,而没有传递给该异步函数任何的参数,也不需要获取该异步函数的结果。如果主线需要传递给该异步函数一个参数,并且还要在该异步函数执行完毕之后获取其执行结果,那应该如何实现呢?
 class Program
{
public delegate int DoWord(int count);
static void Main(string[] args)
{
DoWord d = new DoWord(WorkPro);
IAsyncResult r= d.BeginInvoke(,null,null);//no.1
int result= d.EndInvoke(r);//no.2
Console.WriteLine(result);
for (int i = ; i < ; i++)//no.3
{
Thread.Sleep();//主线程需要做的事
}
Console.WriteLine("主线程done");
Console.ReadKey();
}
public static int WorkPro(int count)
{
int sum = ;
//做一些耗时的工作
for (int i = ; i < count; i++)
{
sum += i;
}
return sum;
}
我们已经把委托类型改为具有一个int类型的参数和int类型返回值。在这里解释一下,每当你的编译器发现定义了一个委托类型,就会对应的生成一个类型,并且该类型BeginInvoke方法的参数个数也是不同的,本例声明的委托类型为:
public delegate int DoWord(int count);
实际生成的BeginInvoke原型为:IAsyncResult   BeginInvoke(int count, AsyncCallBack callback, object @object)
在no.1处还是和第一个例子一样调用委托,不同的是用IAsyncResult接口的变量接收了异步调用(并不是异步函数)的返回状态,这是方便后面调用EndInvoke方法接受这个异步函数调用结果而使用的,也可以通过该参数查看异步函数执行的状态,该接口有一个IsCompleted的属性。在no.2处使用d.EndInvoke(r)来接受异步函数返回值的。必须指出的是,主线程在调用委托的EndInvoke(r)方法时,当异步函数没有执行完毕的话,主线程会一直处于阻塞,等待异步函数执行完毕,获取返回值之后才执行no.3的for循环。这样就还会导致主线程处于阻塞状态。
理想的状态的是,当异步函数调用完成之后,自动通知任务执行完成。当然委托也能够做到,这就要使用BeginInvoke方法的后两个参数啦。看下面这个例子。
class Program
{
public delegate int DoWord(int count);
static void Main(string[] args)
{
DoWord d = new DoWord(WorkPro);
IAsyncResult r= d.BeginInvoke(,CallBack ,d);//no.1
for (int i = ; i < ; i++)
{
Thread.Sleep();//主线程需要做的事
}
Console.WriteLine("主线程done");
Console.ReadKey();
}
public static int WorkPro(int count)
{
int sum = ;
//做一些耗时的工作
for (int i = ; i < count; i++)
{
sum += i;
Thread.Sleep();
}
return sum;
} public static void CallBack(IAsyncResult r)
{
DoWord d = (DoWord)r.AsyncState;
Console.WriteLine("异步调用完成,返回结果为{0}", d.EndInvoke(r));
}
}
首先来解释一下BeginInvoke方法的第二个参数是AsyncCallBack 类型的委托(回调函数),当该参数不为空,那么在异步函数执行完毕之后,会调用该委托;第三个参数Object 类型的,代表传递给回调函数的异步调用状态。CallBack回调函数必须带有一个IAsyncResult 类型的参数,通过这个参数可以在回调方法内部获取异步调用的结果。在no.1出就给BeginInvoke函数传递了回调函数CallBack,和委托d,当异步数WorkPro执行完毕之后,就立即通知CallBack回调函数来显示执行结果。这下主线程就不需要阻塞一直的等待异步函数的结果,大大的提升了程序的运行效率。在.net还提供许多类的BeinXXX()EndXXX()的异步版本,比如文件的读写等,具体可以查阅相关的资料。
其中异步函数内部所使用的线程均是线程池中的工作线程,由线程池去分配管理的。
 
二、多线程模型

.net在System.ThreadingSystem.Threading.Tasks这两个命名空间中提供了ThreadThreadPool,和Task三个类来处理多线程的问题,其中Thread是建立一个专用线程,ThreadPool是使用线程池中工作线程,而Task类是采用任务的方式,其内部也是使用线程池中的工作线程。本节只讲Tread类和Tasks类的使用以及其优劣。

1、Thread类
Thread类的使用方法很简单,它开辟的是一个专用线程,不是线程池中的工作线程,不由线程池去管理。该类提供4个重载版本,常见的使用前面两个就好了。
1)public Thread( ThreadStart start ):其中ThreadStart是一个无参无返回值的委托类型。
2)public Thread( ParameterizedThreadStart start ):其中ParameterizedThreadStart 是一个带有一个Object类型的参数,无返回值的委托类型。
Thread类提供了两个构造函数可以看出,Thread类能够异步调用无参无返回值的函数,也能够异步调用带一个Object类型的无返回值的函数。下面就给出一个例子简单的演示一下如何使用Thread异步执行一个带参数的函数。

class Program
{
static void Main(string[] args)
{
Thread t = new Thread(WorkPro);//no.1
t.IsBackground = true;//no.2
t.Start();//no.3
}
public static void WorkPro(object t)
{
//做一些耗时的工作
int count=(int)t;
for (int i = ; i < count; i++)
{
Thread.Sleep();
} Console.WriteLine("任务处理完成");
}
}
no.1实例化一个Thread对象,给传入一个ParameterizedThreadStart 类型的委托;no.2将建立的专用线程设置为后台的任务线程(后台线程会随着调用线程(即使任务没完成)的终止而强制终止,而前台线程如果任务没有处理完,是不会随着调用线程的终止而终止的);no.3调用Start(1000)方法,其中1000是传递给异步执行函数的参数。记住,如果构造Thread对象是ThreadStart委托,那么Start()就直接调用,否则会出现异常。只需要简单的几行代码就能实现函数的异步调用。
其中,当异步函数中处理需要多个参数时,那么只需要建立一个参数类,参数类中包括你函数需要的参数个数,然后将这个参数类传递给异步函数即可。
 
Thread类的使用虽然简单,但是它还是有一定的劣势的,一般不推荐使用。
1)Thread类创建的是一个专用线程,建立一个专用线程是非常耗用系统的资源,建议是使用线程池中的线程。
2)Thread类不能很好的和调用线程进行交互,当任务完成时不能及时的通知,在调用线程也不能随时的取消正在进行的任务。
另外在以下情况下,就只能选择使用Thread类了。
1)执行任务的线程要以非普通的优先级去执行,因为线程池的线程都是以普通优先级运行的。
2)执行任务的线程要表现为一个前台线程,因为线程池的线程始终都是一个后台线程。
3)异步执行的任务需要长时间的,那么就可以使用Thread类为该任务建立一个专用线程。
 

2、Task类

Task类是封装的一个任务类,内部使用的是ThreadPool类,提供了内建机制,让你知道什么时候异步完成以及如何获取异步执行的结果,并且还能取消异步执行的任务。下面看一个例子是如何使用Task类来执行异步操作的。
 class Program
{
static void Main(string[] args)
{
Task t = new Task((c) =>
{
int count = (int)c;
for (int i = ; i < count; i++)
{
Thread.Sleep();
}
Console.WriteLine("任务处理完成");
}, );//no.1
t.Start(); for (int i = ; i < ; i++)
{
Thread.Sleep();
}
Console.WriteLine("done");
}
}
no.1处使用Task的构造函数为:
public Task( Action<Object> action, Object state )一个Action<Object>类型的委托(即异步调用函数具有一个Object类型的参数),和一个Object类型的参数,也就是传递给异步函数的参数,Task类还有几种方式的重载,我们还可以传递一些TaskCreationOptions标志来控制Task的执行方式。在这里我使用的是lambda表达去写委托的,这样使得程序的结构更加的清晰,使用Start()来启动异步函数的调用。
--------
如果需要异步函数有返回值,那么此时就需要使用Task<TResult>泛型类(派生自Task)来实现,其中TResult代表返回的类型。因为异步函数具有返回值,所以Task<TResult>的各种重载版本的构造函数第一个委托类型的参数都是Fun<TResult>或者Fun<Object,TResult>。下面演示等待任务完成并获取其结果。
 class Program
{
static void Main(string[] args)
{
Task<int> t = new Task<int>((c) =>
{
int count = (int)c;
int sum=;
for (int i = ; i < count; i++)
{
Thread.Sleep();
sum+=i;
}
Console.WriteLine("任务处理完成");
return sum;
}, );
t.Start();
t.Wait();//no.1
Console.WriteLine("任务执行的结果{0}", t.Result);//no.2
for (int i = ; i < ; i++)
{
Thread.Sleep();
}
Console.WriteLine("done");
}
}
如果任务中出现了异常,那么异常会被吞噬掉,并存储到一个集合中去,而线程可以返回到线程池中去。但是如果在代码中调用了Wait方法或者是Result属性,任务有异常发生就会被引发,不会被吞噬掉。其中Result属性内部本身也调用了Wati方法。Wait方法和上一节中的委托的EndInvoke方法类似,会使得调用线程阻塞直到异步任务完成。下面我们会介绍如何避免获取异步结果的阻塞情况,在讲解之前,先说一下,如何取消正在运行的任务。
------
看下面一段代码如何演示取消正在运行的任务。
class Program
{
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();//no.1
Task<int> t = new Task<int>((c) =>Sum(cts.Token ,(int)c), );//no.2
t.Start();
cts.Cancel();//no.3如果任务还没完成,但是Task有可能完成啦
for (int i = ; i < ; i++)
{
Thread.Sleep();
}
Console.WriteLine("done");
}
static int Sum(CancellationToken ct, int count)
{
int sum = ;
for (int i = ; i < count; i++)
{
if (!ct.CanBeCanceled)
{
Thread.Sleep();
sum += i;
}
else
{
Console.WriteLine("任务取消");
//进行回滚操作
return -;//退出任务
}
}
Console.WriteLine("任务处理完成");
return sum;
}
}
取消任务要引用一个CancellationTokenSource 对象。在需要异步执行的方法中增加一个CancellationToken类型的形参。然后在异步函数的for循环代码中用一个if语句判断CancellationTokenCanBeCanceled属性,这个属性可以用来判断在调用线程是否取消任务的执行,除CanBeCanceled属性之外,还可以使用ThrowIfCancellationRequested方法,该方法的作用是如果在调用线程调用CancellationTokenSource对象的Cancel方法,那么就会引发一个异常,然后在调用线程进行捕捉就好了,这是在异步函数中的处理方式。no.1在构建任务之前需要建立一个CancellationTokenSource ,no2.并且把CancellationTokenSource传递给异步调用函数,传递的是CancellationTokenSource对象的Toke属性,该属性是一个CancellationToken类型的对象。这样就完成任务的取消模式,如果想在调用线程中取消任务的执行,只需要调用CancellationTokenSource Cancel方法就行啦。
 
------
前面就说过了,获取任务结果调用Wait方法和Result属性导致调用线程阻塞,那么如何处理这种情况呢,这就使用了Task<TResult>类提供的ContinueWith方法。该方法的作用是当任务完成时,启动一个新的任务,不仅仅是如此,该方法还有可以在任务只出现异常或者取消等情况的时候才执行,只需要给该方法传递TaskContinuationOptions枚举类型就可以了。下面就演示一下如何使用ContinueWith方法。
首先看下ContinueWith方法的原型。
public Task ContinueWith( Action<Task> continuationAction )采用一个Action<Task>类型的委托。该方法提供了多种重载的版本,这只是最简单的一种。
 

public Task ContinueWith( Action<Task> continuationAction, TaskContinuationOptions continuationOptions )第二个参数代表新任务的执行条件,当任务满足这个枚举条件才执行 Action<Task>类型的回调函数。

代码如下:
class Program
{
static void Main(string[] args)
{
Task<int> t = new Task<int>((c) =>Sum((int)c), );
t.Start();
t.ContinueWith(task => Console.WriteLine("任务完成的结果{0}", task.Result));//当任务执行完之后执行
t.ContinueWith(task => Console.WriteLine(""), TaskContinuationOptions.OnlyOnFaulted);//当任务出现异常时才执行
for (int i = ; i < ; i++)
{
Thread.Sleep();
}
Console.WriteLine("done");
}
static int Sum( int count)
{
int sum = ;
for (int i = ; i < count; i++)
{
Thread.Sleep();
sum += i;
}
Console.WriteLine("任务处理完成");
return sum;
}
}
t.Start()之后调用第一个ContinueWith方法,该方法第一参数就是一个Action<Task>的委托类型,相当于是一个回调函数,在这里我也用lambda表达式,当任务完成就会启用一个新任务去执行这个回调函数。而第二个ContinueWith里面的回调方法却不会执行,因为我们的任务也就是Sum方法不会发生异常,不能满足TaskContinuationOptions.OnlyOnFaulted这个枚举条件。这种用法比委托的异步函数编程看起来要简单些。最关键的是ContinueWith的还有一个重载版本可以带一个TaskScheduler对象参数,该对象负责执行被调度的任务。FCL中提供两种任务调度器,均派生自TaskScheduler类型:线程池调度器,和同步上下文任务调用器。而在Winform窗体程序设计中TaskScheduler尤为有用,为什么这么说呢?因为在窗体程序中的控件都是有ui线程去创建,而我们所执行的后台任务使用线程都是线程池中的工作线程,所以当我们的任务完成之后需要反馈到Winform控件上,但是控件创建的线程和任务执行的线程不是同一个线程,如果在任务线程中去更新控件就会导致控件对象安全问题会出现异常。所以操作控件,就必须要使用ui线程去操作。因此在ContinueWith获取任务执行的结果的并反馈到控件的任务调度上不能使用线程池任务调用器,而要使用同步上下文任务调度器去调度,即采用ui这个线程去调用ContinueWith方法所绑定的回调用函数即Action<Task>类型的委托。下面将使用任务调度器来把异步执行的Sum计算结果反馈到Winform界面的TextBox控件中。
界面如下。

代码如下。
 public partial class Form1 : Form
{
private readonly TaskScheduler contextTaskScheduler;//声明一个任务调度器
public Form1()
{
InitializeComponent();
contextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();//no.1获得一个上下文任务调度器
} private void button1_Click(object sender, EventArgs e)
{
Task<int> t = new Task<int>((n) => Sum((int)n),);
t.Start();
t.ContinueWith(task =>this.textBox1 .Text =task.Result.ToString(),contextTaskScheduler);//当任务执行完之后执行
t.ContinueWith(task=>MessageBox .Show ("任务出现异常"),CancellationToken.None ,TaskContinuationOptions.OnlyOnFaulted,contextTaskScheduler );//当任务出现异常时才执行
}
int Sum(int count)
{
int sum = ;
for (int i = ; i < count; i++)
{
Thread.Sleep();
sum += i;
}
Console.WriteLine("任务处理完成");
return sum;
}
}
在no.1窗体的构造函数获取该UI线程的同步上下文调度器。在按钮的事件接受异步执行的结果时候,都传递了contextTaskScheduler同步上下文的调度器,目的是,当异步任务完成之后,调度UI线程去执行任务完成之后的回调函数。
------
到目前为止,我平常用到的异步编程模式也就这么多了,当然Task类的ContinueWith还有很多重载的版本,会提供不一样效果。在开篇的时候就说,如何在调用线程中实时获取异步任务的执行情况,比如我的任务是插入100w条数据到数据库,我在界面中需要实时的刷新数据导入的进度条,这种情况使用上述所讲的是做不到的。具体如何做到,我在另外一篇文章已经详细的讲过啦,采用回调函数的方法(委托)来实现,链接:http://www.cnblogs.com/mingjiatang/p/5079632.html
 
三、小结
虽然在.net中提供了众多的异步编程模式,但是推荐最好使用Task类,因为Task类使用线程池中的任务线程,又由线程池管理,效率相对来说较高,而且Task类内部有比较好的机制,能让调用线程与任务进行交互。反正不管用哪种模式,总之尽量不要出现阻塞的情况,只要程序中出现线程阻塞,线程池就会创建新的活动线程,因为线程池总是要保证活动的任务线程数量与CPU的核数一致,它觉得这样性能最佳,当阻塞的线程恢复正常之后,线程池又会将多余的线程销毁,避免系统调度线程时频繁的进行上下文切换。这样的创建、销毁线程是非常的浪费系统资源影响性能的。而在线程同步的时候常常会出现阻塞的情况,所以能设计不用线程同步去解决问题,尽量不用线程同步。最后要是有写的不对的地方,请各位指正,谢谢!

.NET中的异步编程的更多相关文章

  1. .Net中的异步编程总结

    一直以来很想梳理下我在开发过程中使用异步编程的心得和体会,但是由于我是APM异步编程模式的死忠,当TAP模式和TPL模式出现的时候我并未真正的去接纳这两种模式,所以导致我一直没有花太多心思去整理这两部 ...

  2. C#中的异步编程Async 和 Await

    谈到C#中的异步编程,离不开Async和Await关键字 谈到异步编程,首先我们就要明白到底什么是异步编程. 平时我们的编程一般都是同步编程,所谓同步编程的意思,和我们平时说的同时做几件事情完全不同. ...

  3. .NET中的异步编程——常见的错误和最佳实践

    在这篇文章中,我们将通过使用异步编程的一些最常见的错误来给你们一些参考. 背景 在之前的文章<.NET中的异步编程——动机和单元测试>中,我们开始分析.NET世界中的异步编程.在那篇文章中 ...

  4. javaScript中的异步编程模式

    1.事件模型 let button = document.getElementById("my-btn"); button.onclick = function(event) { ...

  5. Netty 中的异步编程 Future 和 Promise

    Netty 中大量 I/O 操作都是异步执行,本篇博文来聊聊 Netty 中的异步编程. Java Future 提供的异步模型 JDK 5 引入了 Future 模式.Future 接口是 Java ...

  6. 一文说通C#中的异步编程

    天天写,不一定就明白. 又及,前两天看了一个关于同步方法中调用异步方法的文章,里面有些概念不太正确,所以整理了这个文章.   一.同步和异步. 先说同步. 同步概念大家都很熟悉.在异步概念出来之前,我 ...

  7. 一文说通C#中的异步编程补遗

    前文写了关于C#中的异步编程.后台有无数人在讨论,很多人把异步和多线程混了. 文章在这儿:一文说通C#中的异步编程 所以,本文从体系的角度,再写一下这个异步编程.   一.C#中的异步编程演变 1. ...

  8. promise 的基本概念 和如何解决js中的异步编程问题 对 promis 的 then all ctch 的分析 和 await async 的理解

    * promise承诺 * 解决js中异步编程的问题 * * 异步-同步 * 阻塞-无阻塞 * * 同步和异步的区别? 异步;同步 指的是被请求者 解析:被请求者(该事情的处理者)在处理完事情的时候的 ...

  9. 全面解析C#中的异步编程

    当我们处理一些长线的调用时,经常会导致界面停止响应或者IIS线程占用过多等问题,这个时候我们需要更多的是用异步编程来修正这些问题,但是通常都是说起来容易做起来难,诚然异步编程相对于同步编程来说,它是一 ...

随机推荐

  1. 程序猿的日常——SpringMVC系统架构与流程回顾

    web开发经历了很漫长的时间,在国内也快有十几年的时间了.从最开始的进程级到现在的MVC经历了很多的改进和优化,本篇就主要复习了解下Spring MVC相关的知识. 发展历程 第一阶段 CGI进程响应 ...

  2. 变不可能为可能 - .NET Windows Form 改变窗体类名(Class Name)有多难?续篇

    发布<.NET Windows Form 改变窗体类名(Class Name)有多难?>转眼大半年过去了,要不是在前几天有园友对这篇文章进行评论,基本上已经很少关注它了,毕竟那只是一个解惑 ...

  3. linux parallel rsync 拷贝N多文件

    先来个对比图看一下, 左边图是普通 rsync 目录拷贝, 右边图是借助 parallel 工具并发起了多个 rsync centos6.5安装 parallel #!/bin/bash # Inst ...

  4. 【bzoj1855】 [Scoi2010]股票交易 单调队列优化DP

    上一篇blog已经讲了单调队列与单调栈的用法,本篇将讲述如何借助单调队列优化dp. 我先丢一道题:bzoj1855 此题不难想出O(n^4)做法,我们用f[i][j]表示第i天手中持有j只股票时,所赚 ...

  5. Redis客户端使用

    http://wenku.baidu.com/view/6ccd650af12d2af90242e63d.html 一.下载jedis 代码 jedis 代码地址:https://github.com ...

  6. (转)Db2数据库一次生产故障详细记录---数据库坏页

    原文:http://www.talkwithtrend.com/Article/216335 前言 数据库最严重的故障莫过于数据库损坏.数据库坏页是数据库损坏的一种,如果数据库中有数据页出现损坏,在没 ...

  7. Android 手势识别—缩放

    上一篇讲解了手势识别中的点击和双击事件的识别,用到的是GestureDetector类和GestureDetectorCompat类,用于监听用户触摸屏幕中的简单动作. 缩放 基本用法如下,可以通过缩 ...

  8. ViewPage最全解析

    简单说明: ViewPager是android扩展包v4包中的类,直接继承了ViewGroup类,和LinearLayout等布局一样,都是一个容器,需要在里面添加我们想要显示的内容. 一.在xml中 ...

  9. Eclipse中Maven项目出现红色感叹号问题

    在Eclipse环境下,有时我们会遇到Maven项目出现红色感叹号的情形,而一旦项目出现感叹号,Eclipse便无法帮我们进行自动编译等工作,尽管有时候不会影响运行,但每次只能手动启动Maven重新编 ...

  10. linux xargs 命令详解

    xargs是给命令传递参数的一个过滤器,也是组合多个命令的一个工具.它把一个数据流分割为一些足够小的块,以方便过滤器和命令进行处理.通常情况下,xargs从管道或者stdin中读取数据,但是它也能够从 ...