浅谈.NET下的多线程和并行计算(九)Winform中多线程编程基础下
在之前的文章中我们介绍过两种Timer和BackgroundWorker组件,在上文中我们提到过,强烈建议在UI线程上操作控件,否则很容易产生人品问题。可以想到,上次介绍的两个Timer基于ThreadPool,回调方法运行于不同于UI线程的新线程上,在这个方法中操作控件需要进行Invoke或BeginInvoke。其实,还有第三种System.Windows.Forms.Timer,它可以让回调事件在UI线程上执行,我们来做一个实验比较一下System.Windows.Forms.Timer和System.Timers.Timer。在一个窗体上新建两个标签控件,然后来创建两个计时器:
private System.Windows.Forms.Timer timer1 = new System.Windows.Forms.Timer(); private System.Timers.Timer timer2 = new System.Timers.Timer(); public Form2() { InitializeComponent(); }
双击表单,在Load事件中写如下代码:
this.Text = string.Concat("#", Thread.CurrentThread.ManagedThreadId); //timer2.SynchronizingObject = this; timer2.Interval = 1000; timer2.Elapsed += new System.Timers.ElapsedEventHandler(timer2_Elapsed); timer2.Enabled = true; timer1.Interval = 1000; timer1.Tick += new System.EventHandler(this.timer1_Tick); timer1.Enabled = true;
然后是两个计时器的处理方法:
private void timer1_Tick(object sender, EventArgs e) { label1.Text = string.Format("timer1 : #{0} {1} {2}", Thread.CurrentThread.ManagedThreadId, this.InvokeRequired, DateTime.Now.ToString()); } private void timer2_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { label2.Text = string.Format("timer2 : #{0} {1} {2}", Thread.CurrentThread.ManagedThreadId, this.InvokeRequired, DateTime.Now.ToString()); }
以非调试方式运行程序,可以看到如下的结果:
分析一下这个结果
1) UI托管线程Id是1
2) System.Windows.Forms.Timer运行于UI线程,不需要Invoke
3) System.Timers.Timer运行于新的线程,需要Invoke
也就是说如果以调试方式运行程序的话,label2控件的赋值操作将会失败。要解决这个问题有两个办法:
1) 取消注释 timer2.SynchronizingObject = this; 一句使回调方法运行于UI线程(等同于System.Windows.Forms.Timer的效果)
2) 在回调方法内涉及到UI的操作使用Invoke或BeginInvoke
您可以实验一下,在回调方法中阻塞线程的话,UI被阻塞,界面停止响应,因此,如果您的回调方法需要比较耗时操作的话建议使用灵活性更大的System.Timers.Timer,手动使用Invoke或BeginInvoke,当然如果回调方法主要用于操作UI可以直接使用System.Windows.Forms.Timer。
对于BackgroundWorker组件我们可以想到通常会希望后台操作运行于工作线程上,而进度汇报的方法运行于UI线程上,其实它就是这么做的,我们可以直接在工具箱中拖一个BackgroundWorker到表单设计器上,然后在表单上添加如下控件:
分别是一个ProgressBar,两个按钮和两个标签。对于BackgroundWorker,我们直接通过属性窗口打开它的进度汇报和取消功能:
然后添加三个事件的处理方法:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { for (int i = 0; i <= 100; i += 10) { if (backgroundWorker1.CancellationPending) { for (int j = i; j >=0; j -= 10) { backgroundWorker1.ReportProgress(j); Thread.Sleep(500); } e.Cancel = true; return; } label1.Text = string.Format("worker : #{0} {1} {2}", Thread.CurrentThread.ManagedThreadId, this.InvokeRequired, DateTime.Now.ToString()); backgroundWorker1.ReportProgress(i); Thread.Sleep(500); } e.Result = 100; } private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { label2.Text = string.Format("progress : #{0} {1} {2}", Thread.CurrentThread.ManagedThreadId, this.InvokeRequired, DateTime.Now.ToString()); progressBar1.Value = e.ProgressPercentage; } private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Cancelled) MessageBox.Show("Cancelled"); else if (e.Error != null) MessageBox.Show(e.Error.ToString()); else MessageBox.Show(e.Result.ToString()); }
两个按钮分别用于开始后体工作和取消后台工作:
private void button1_Click(object sender, EventArgs e) { if (!backgroundWorker1.IsBusy) backgroundWorker1.RunWorkerAsync(); } private void button2_Click(object sender, EventArgs e) { if (backgroundWorker1.IsBusy) backgroundWorker1.CancelAsync(); }
在DoWork事件处理方法和ProgressChanged事件处理方法中我们在相应的标签上输出托管线程Id。在DoWork事件处理方法中,我们也实现了取消任务后的回滚操作,以非调试方式运行程序并且点击开始按钮后的效果如下:
可以看到,ProgressChanged事件处理方法运行于UI线程,而DoWork事件处理方法运行于新线程上,这符合我们的期望。
本质上,BackgroundWorker的进度汇报基于AsyncOperation,差不多如下:
AsyncOperation asyncOperation; public Form4() { InitializeComponent(); this.Text = string.Concat("#", Thread.CurrentThread.ManagedThreadId); asyncOperation = AsyncOperationManager.CreateOperation(null); } private void button1_Click(object sender, EventArgs e) { new Thread(() => { for (int i = 0; i <= 100; i += 10) { label1.Text = string.Format("worker : #{0} {1} {2}", Thread.CurrentThread.ManagedThreadId, this.InvokeRequired, DateTime.Now.ToString()); asyncOperation.Post(new SendOrPostCallback((j)=> { label2.Text = string.Format("progress : #{0} {1} {2}", Thread.CurrentThread.ManagedThreadId, this.InvokeRequired, DateTime.Now.ToString()); progressBar1.Value = (int)j; }), i); Thread.Sleep(500); } }) { IsBackground = true }.Start(); }
而AsyncOperation又是基于SynchronizationContext的封装。个人认为AsyncOperation和SynchronizationContext达到的效果和Invoke以及BeginInvoke差不多,而且MSDN上也并没有很多介绍AsyncOperation和SynchronizationContext的篇幅,因此,在不使用BackgroundWorker的时候我还是更愿意去直接使用Invoke和BeinInvoke这种原始的方法。
后面几节我们将介绍.NET异步编程模型APM。