C#异步编程
在使用多线程编写端口扫描程序时,我自己感觉同步和确定所有线程都执行完的时间是2个比较麻烦的问题。有园友评论说现在已经不手动创建thread对象了,而是直接使用Task异步方式,我的网络编程老师也讲到了异步编程的优越性。在学习了课本上的知识后,进行了一个总结分享给大家。从.NET4.5开始,用async和await关键字再加上Task.Run是一个非常不错的异步编程模型。
1.await和async
异步模式从技术上看就是利用委托来实现的,它的主要好处是在异步执行的过程中,用户仍然可以操控UI界面。使用Task类和使用Thread类有很多相似的地方,Task类也是通过调用方法去实现一个任务的完成,方法可是是命名方法或匿名方法,在执行过程中可使用async和await来实现异步执行。async是一个修饰符,它只能用在方法或者事件处理程序的签名中。对于方法可分为有返回值和无返回值两种情况,事件则只有一种,如下面三条语句所示:
private async Task<int> MethodAsync();//有返回值的异步方法
private async Task MethodAsync();//无返回值的异步方法
private async void btnOk_Click();//异步事件处理程序
await是一个运算符,它表示等待异步执行的结果。也可以理解为await运算符实际上是对方法的返回值进行操作,也就是对Task<Result>进行操作,而不是对方法本身进行操作。还有一点要注意,await是一定要放在异步方法的内部,如果没有放在内部的话,VS会自动报错。以下是async和await使用的例子:
private async void button5_Click(object sender, EventArgs e)
{
Task a = Method1Async();
//此处可继续执行其他代码
await a;//等待任务a完成
Task<int> b = Method2Async();
//此处可继续执行其他代码
int c = await b;//等待任务b完成,且可以拿到任务b的返回值
}
Task Method1Async();
async Task<int> Method2Async()
{
await Task.Delay(100);
return 1;
}
await和同步编程最大的不同之处是:异步等待任务完成的时候,在不会继续执行后面的代码时,也不会影响界面的操作。在.NET提供的类中,异步方法都是约定用Async作为后缀,这样可以很清楚的知道这个方法是异步方法还是同步方法。
2. 创建任务
创建任务也就是将任务与要执行的方法联系起来,编写任务执行的方法时,这个方法既可以是同步方法也可以是异步方法,还可以是匿名方法。执行异步方法时,必须用async和Task共同表示没有返回值的任务,用async和Task<TResult>共同表示返回值为TResult的任务。以下是定义执行任务的方法。
private async void button5_Click(object sender, EventArgs e)
{
//Task.Run方法表示使用默认的任务调度程序在线程池中通过后台执行指定的任务
//如果不需要自己去调度方法,使用这个方式最方便
await Task.Run(()=>Method1Async());//执行同步方法
int c = await Task.Run(()=>Method2Async());//执行异步方法
await Task.Run(async () => { c = 2; });//执行异步匿名方法
}
void Method1Async();
async Task<int> Method2Async(){...}
Task.Run方法常用的重载形式有以下4种,另外它也是可以用new关键字显示创建任务,但是这种方式用的不多。
Task Run(Func<Task> function);//执行不带返回值的任务
Task<TResult> Run<TResult>(Func<Task<TResult>> function);//执行带返回值的任务
Task<TResult> Run<TResult>(Func<Task<TResult>> function, CancellationToken cancellationToken);//执行过程中可以监听取消通知
Task Run(Func<Task> function, CancellationToken cancellationToken);//执行过程中可以监听取消通知
3. 终止任务
在执行任务时肯定会出现需要终止任务的情况,这里的终止告诉任务你要尽快停下来不再执行了,而不是直接销毁任务实例。这里可以打个比方,学生一起出去吃饭了,学生与老师都在班群里面,突然班群里老师说要让同学们集合,如果所有同学都看到这个消息,然后学生们开始出发,这样就可以正确的集合。上面的例子有一个很重要的前提,那就是所有同学都要看到这个消息,也就是学生是时刻监听消息的。CancellationTokenSource类和CancellationToken结构用于实现多线程、线程池和Task任务的取消操作,处理模式与上面的例子相似。创建的班群就是CancellationTokenSource对象,收到的通知就是CancellationToken对象。CancellationTokenSource用于创建取消通知,CancellationToken则用于传播应取消操作的通知,当调用任务前,可以先创建取消源对象CancellationTokenSource cts=new CancellationTokenSource();,如果希望在30秒后自动发出取消通知,可以传入参数CancellationTokenSource(TimeSpan.FromSeconds(30));CancellationToken ct=cts.Token;,后一句代码是拿到取消的通知。CancellationTokenSource还有一个Cancel方法,将这个属性设为true时,该方法会将所有添加了取消标记的CancellationToken对象的IsCancellationRequested属性都设置为true,这样取消通知就传递到了正在执行的任务。
任务收到取消通知后,可以选择两种方式来终止操作。第一种方式是简单的从委托返回。这种实现方式类似于在调用任务的代码中一个bool值来表示取消通知,任务收到后就直接返回了。当采用这种方式时任务状态的返回值为TaskStatus.RanToCompletion枚举值,它表示正常完成,而不是TaskStatus.Canceled枚举值。第二种方式是在代码里引发OperationCanceledException异常,并将其传递到在其上请求了取消的标记,采用这种方式取消的任务会转换为用Canceled枚举值表示的状态。完成引发异常的首选方式是调用ct.ThrowIfCancellationRequestes();。以下是代码示例,写了一个winform程序,利用进度条来取消任务。第一个图是没有引发异常时,程序退出for循环,执行后面的代码后返回了,第二张图是第二种方式,引发了异常后直接跳转到catch语句块了。
CancellationTokenSource cts; private async void button3_Click(object sender, EventArgs e) { progressBar1.Maximum = 100; progressBar1.Value = 0; cts = new CancellationTokenSource(); var aa = MYThreadAsync("a", cts.Token); try { await aa; listBox1.Items.Add("await后面"); } catch { if (aa.IsCanceled) listBox1.Items.Add("a取消"); } } private void button4_Click(object sender, EventArgs e) { cts.Cancel(); } public async Task MYThreadAsync(string s, CancellationToken ct) { for (int i = 0; i < 50; i++) { if (ct.IsCancellationRequested) break; //点击关闭按钮,IsCancellationRequested就为true,就会退出for循环,这是第一种方式 progressBar1.Value += 2; await Task.Delay(100); ct.ThrowIfCancellationRequested();//这是第二种方式,它会终止任务并且返回catch语句块里面 } listBox1.Items.Add("任务" + s + "完成了"); }
4. 获取任务执行的状态
在异步编程中,很显然任务执行的状态是一个非常重要的参数。在任务的生命周期里,可以通过Status属性来获取任务执行的状态,当任务完成后还可以通过任务属性知道任务完成的情况。可利用任务实例的Status属性获取任务执行的状态,任务执行的状态用TaskStatus枚举表示,以下是TaskStatus的枚举值:
Created:任务已经初始化,但尚未进入调度计划
WaitingForActivation:该任务已进入调度计划,正在等待被调度程序激活
WaitingToRun:该任务已被调度程序激活,但尚未开始执行
Running:该任务正在运行,但尚未完成
RanToCompletion:该任务已经成功完成
Canceled:该任务由于被取消而完成,引发异常或调用方已向该任务的CancellationToken发出信号
Faulted:该任务因为出现未经处理的异常而完成
WaitingForChildrenToComplete:该任务本身已完成,正等待附加的子任务完成
任务完成情况相关的属性有IsCompleted、IsCanceled和IsFaulted等属性,从单词意思上看不难理解它们的意思,其中要注意IsCompleted属性表示任务是否完成,无论是正常结束还是因为取消或异常而完成都为完成。
5. 任务执行的进度
有时候我们希望让某些异步操作提供进度通知,以便在界面中显示异步操作执行的进度,可以用Progress<T>类来得到任务执行的进度。以下是利用方法里的Report方法将方法内变量的值传回创建任务的事件代码里,从而更新进度条的值。
CancellationTokenSource cts; private async void button3_Click(object sender, EventArgs e) { progressBar1.Maximum = 100; progressBar1.Value = 0; cts = new CancellationTokenSource(); CancellationToken ct = cts.Token; var pp = new Progress<int>(); pp.ProgressChanged += (s, n) => { progressBar1.Value = n; }; var tt = Task.Run(()=>MYThreadAsync(pp,cts.Token,500),cts.Token); try { await tt; if (tt.Exception == null) listBox1.Items.Add("任务完成"); } catch (Exception ex) { listBox1.Items.Add("异常" + ex.Message); } } private void button4_Click(object sender, EventArgs e) { cts.Cancel(); } public void MYThreadAsync(IProgress<int> progress, CancellationToken ct, int delay) { int p = 0;//进度 while (p < 100 && ct.IsCancellationRequested == false) { p += 1; Thread.Sleep(delay); progress.Report(p);//这个方法将会触发ProgressChanged事件更新进度条 } }
6. 定时完成任务
无论是服务器还是客户端,都是有定时完成某个任务的需要的。System.Timers.Timer类是一个不错的定时设置类,这个类可以引发事件,但它默认是在线程池中引发事件,而不是在当前线程中引发事件。Timer类的常用属性有AutoReset和Interval属性,AutoReset是获取或设置一个bool值,该值为true表示每次间隔结束时都引发一次Elapsed事件,false表示仅在首次间隔结束时引发一次该事件。Interval属性是获取或设置两次Elapsed事件的间隔时间,该值必须大于零并小于Int.MaxValue,默认值为100毫秒。Timer类还有两个常用方法那就是Start和Stop方法。
还有一个System.Threading.Timer类,它也是在线程池中定时执行任务,它与前一个Timer类的区别是该类不使用事件模型,而是直接通过TimerCallback类型的委托来实现的。该类的构造函数为:Timer(TimerCallback callback,Object state,TimeSpan douTime,TimeSpan period)。callback表示要执行的方法,state表示一个包含回调方法要使用的信息的对象,dueTime是首次调用回调方法之前延迟的时间,period表示每次调用回调方法的时间间隔,-1表示终止。这样创建对象后,首次到达dueTime延时时间会自动调用一次callback委托,以后每隔period时间间隔调用一次。以下是这两种方式的运行效果和源代码。
System.Timers.Timer timer; System.Threading.Timer threadtimer; private void button2_Click(object sender, EventArgs e)//Timers.Timer { progressBar1.Maximum = 100; progressBar1.Value = 0; int pro=0; timer = new System.Timers.Timer(500); timer.AutoReset = true; timer.Elapsed+= (obj, args) => { pro+=5; progressBar1.Value = pro; }; timer.Start(); } private void button5_Click(object sender, EventArgs e) { timer.Stop(); listBox1.Items.Add("第一个已经停止"); } //Threading.Timer类 private void button1_Click(object sender, EventArgs e) { progressBar2.Maximum = 100; progressBar2.Value = 0; TimeSpan dueTime = new TimeSpan(0, 0, 0, 1); TimeSpan period = new TimeSpan(0, 0, 0, 0, 200); System.Threading.TimerCallback timecall = new TimerCallback((obj) => progressBar2.Value += 5); threadtimer = new System.Threading.Timer(timecall, null, dueTime, period); } private void button6_Click(object sender, EventArgs e) { threadtimer.Dispose(); listBox1.Items.Add("第二个已经停止"); }
这篇文章只总结了单个任务的异步执行的基础,还得继续学习多任务并行执行。