浅谈.NET下的多线程和并行计算(七)基于多线程的基本组件
在多线程应用中我们有一些很常见的需求,比如定时去做计划任务,或者是在执行一个长时间的任务,在执行这个任务的过程中能有进度显示(能想到要实现这个需求需要新开一个线程,避免阻塞UI的更新)。对于这些应用.NET提供了现成的组件。
首先来看一下System.Threading的Timer组件,它提供了定时执行某个任务的方法:
ThreadPool.SetMinThreads(2, 2); ThreadPool.SetMaxThreads(4, 4); Timer timer = new Timer((state) => { int a, b; ThreadPool.GetAvailableThreads(out a, out b); Console.WriteLine(string.Format("({0}/{1}) #{2} : {3}", a, b, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("mm:ss"))); }, null, 2000, 1000); Console.WriteLine(DateTime.Now.ToString("mm:ss")); Thread.Sleep(5000); Console.WriteLine("Change()"); timer.Change(3000, 500); Thread.Sleep(5000); Console.WriteLine("Dispose()"); timer.Dispose(); Thread.Sleep(5000); Console.WriteLine(DateTime.Now.ToString("mm:ss"));
这段代码的运行结果如下:
我们可以看到:
1) Timer构造方法中第一个参数就是要定时执行的方法,这个方法接受一个状态参数,第二个参数是状态参数,第三个参数是首次调用回调方法前的延迟毫秒,第四个参数就是执行方法的间隔毫秒
2) 从结果中我们可以看到,第一次回调方法在2秒后执行,然后每一秒执行一次,之后我们调用了Change()方法把延迟时间设置为3秒,把间隔设置为500毫秒,看到Timer在完成了上次回调之后3秒后执行了新的回调,之后间隔500毫秒执行一次。
3) 最后,我们执行了Dispose()方法,在结束最后一次回调之后Timer就再也没有调用回调方法。
4) 在回调方法中我们输出了线程池的可用线程,可以看到Timer基于线程池,也就是Timer基于后台线程。
.NET中还提供了System.Timers.Timer,它封装并增强了System.Threading.Timer:
System.Timers.Timer timer2 = new System.Timers.Timer(); timer2.Elapsed += new System.Timers.ElapsedEventHandler(timer2_Elapsed); timer2.Interval = 1000; Console.WriteLine("Start()"); timer2.Start(); Console.WriteLine(DateTime.Now.ToString("mm:ss")); Thread.Sleep(5000); Console.WriteLine("Stop()"); timer2.Stop(); Thread.Sleep(5000); Console.WriteLine("Change Interval and Start()"); timer2.Interval = 500; timer2.Start(); Thread.Sleep(5000); Console.WriteLine("Dispose()"); timer2.Dispose(); Thread.Sleep(5000); Console.WriteLine(DateTime.Now.ToString("mm:ss"));
static void timer2_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { int a, b; ThreadPool.GetAvailableThreads(out a, out b); Console.WriteLine(string.Format("({0}/{1}) #{2} : {3}", a, b, Thread.CurrentThread.ManagedThreadId, e.SignalTime.ToString("mm:ss"))); }
(假设我们还是设置线程池最小2个线程最大4个线程)
这段代码的结果如下:
从运行结果中我们可以看到:
1) 由于System.Timers.Timer封装了System.Threading.Timer,所以还是基于线程池
2) 默认Timer是停止的,启动后需要等待一个Interval再执行回调方法
最后,再来看看BackgroundWorker,它提供了对于前面说的执行一个任务,在UI上更新进度这种应用的封装,首先定义一个static的BackgroundWorker:
static BackgroundWorker bw = new BackgroundWorker();
然后写如下测试代码:
bw.WorkerReportsProgress = true; bw.WorkerSupportsCancellation = true; bw.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged); bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted); bw.DoWork += new DoWorkEventHandler(bw_DoWork); bw.Disposed += new EventHandler(bw_Disposed); AutoResetEvent are = new AutoResetEvent(false); Console.WriteLine(DateTime.Now.ToString("mm:ss")); bw.RunWorkerAsync(are); Thread.Sleep(2000); are.Set(); Thread.Sleep(2000); Console.WriteLine("CancelAsync()"); bw.CancelAsync(); while (bw.IsBusy) Thread.Sleep(10); bw.Dispose();
这段代码中我们:
1) 设置BackgroundWorker可以汇报进度(通过ProgressChanged事件)
2) 设置BackgroundWorker支持任务取消
3) 定义了进度更新的处理事件:
static void bw_ProgressChanged(object sender, ProgressChangedEventArgs e) { Console.WriteLine(string.Format("{0} : {1}% completed", DateTime.Now.ToString("mm:ss"), e.ProgressPercentage)); }
4) 定义了任务完成的处理事件:
static void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Cancelled) Console.WriteLine("Cancelled"); else if (e.Error != null) Console.WriteLine(e.Error.ToString()); else Console.WriteLine(e.Result); Console.WriteLine(DateTime.Now.ToString("mm:ss")); }
5) 定义了任务的主体方法:
static void bw_DoWork(object sender, DoWorkEventArgs e) { (e.Argument as AutoResetEvent).WaitOne(); for (int i = 0; i <= 100; i += 10) { //if (bw.CancellationPending) //{ // Console.WriteLine("Cancelling..."); // for (int j = 0; j <= 100; j += 20) // { // bw.ReportProgress(j); // Thread.Sleep(500); // } // e.Cancel = true; // return; //} bw.ReportProgress(i); Thread.Sleep(500); } e.Result = 100; }
6) 定义了Dispose BackgroundWorker后的事件:
static void bw_Disposed(object sender, EventArgs e) { Console.WriteLine("disposed"); }
7) 使用信号量作为事件的状态参数让任务延迟2秒执行
8) 主线程通过IsBusy判断任务是否在执行,轮询等待
9) 最后Dispose这个组件
程序执行结果如下:
可以看到:
1) 任务延迟2秒执行,任务分为10个阶段执行,每执行一个阶段汇报一下进度。每个阶段需要500毫秒
2) 任务执行完成之后可以设置Result属性的值,这个值在bw_RunWorkerCompleted中可以获取到
我们还原注释的那些代码来看看取消任务的情况:
我们看到任务在2秒后取消,要注意这种异步取消任务的方式,我们调用了CancelAsync()其实不能从实质上取消任务的执行,要真正取消任务需要在任务的主体方法中不断检测CancellationPending属性,如果为true表示用户希望取消任务,然后去执行一些取消任务的行为,在完成后设置Cancel属性为true,并结束任务主体,这么做的目的是因为对于长时间的任务取消回滚的过程可能也是长时间的,我们同样可以在主体方法中对取消的行为进行进度汇报。您可以自己做相关实验,可以发现在控制台程序中,BackgroundWorker的DoWork可ProgressReport基于两个独立的线程,这两个线程都是基于线程池的,在Winform中 BackgroundWorker的DoWork基于线程池中的独立线程而ProgressReport执行于UI线程。