C#之BackgroundWorker从简单入门到深入精通的用法总结
需求分析
经常用到的耗时操作,例如:
1、文件下载和上载(包括点对点应用程序传输文件,从网络下载文件、图像等)
2、数据库事务(从数据库读到大量的数据到WinForm界面中的DataGridview里呈现)
3、复杂的本地计算
4、本地磁盘文件访问(读写文件,磁盘文件列表)
……
这些操作在长时间运行时会导致用户界面 (UI) 处于停止响应状态,用户在这操作期间无法进行其他的操作,造成非常差的用户体验,为了不使UI层处于停止响应状态,则可以使用 BackgroundWorker 类方便地解决这类问题。这个后台的线程处理,可以很好的实现常规操作的同时,还可以及时通知UI当前处理信息的进度等。
MSDN的介绍
BackgroundWorker是.NET Framework 里用来执行多线程任务的控件,它允许开发人员在一个单独的线程上执行一些操作。耗时的操作(如下载和数据库事务)在长时间运行时可能会导致用户界面 (UI) 似乎处于停止响应状态。 如果您需要能进行响应的用户界面,而且面临与这类操作相关的长时间延迟,则可以使用 BackgroundWorker 类方便地解决问题。
若要在后台执行耗时的操作,请创建一个 BackgroundWorker,侦听那些报告操作进度并在操作完成时发出信号的事件。 可以通过编程方式创建 BackgroundWorker,也可以将它从“工具箱”的“组件”选项卡中拖到窗体上。 如果在 Windows 窗体设计器中创建 BackgroundWorker,则它会出现在组件栏中,而且它的属性会显示在“属性”窗口中。
若要为后台操作做好准备,请添加 DoWork 事件的事件处理程序。 在此事件处理程序中调用耗时的操作。 若要开始此操作,请调用 RunWorkerAsync。 若要收到进度更新的通知,请处理 ProgressChanged 事件。 若要在操作完成时收到通知,请处理 RunWorkerCompleted 事件。
有2点需要注意的:
1、由于DoWork事件内部的代码运行在非UI线程之上,确保在 DoWork 事件处理程序中不操作任何用户界面对象。 而应该通过 ProgressChanged 和 RunWorkerCompleted 事件与用户界面进行通信。
2、BackgroundWorker 事件不跨 AppDomain 边界进行封送处理。 请不要使用 BackgroundWorker 组件在多个 AppDomain 中执行多线程操作。
最简单示例
准备材料:一个耗时的操作
代码如下,这个就不多解释了:
1 2 3 4 5 6 7 8 9 | int iSum = 0; private void button1_Click( object sender, EventArgs e) { for ( int i = 0; i <= 100; i++) { iSum+=i; System.Threading.Thread.Sleep(300); } } |
运行一下,拖动程序界面看看,直接卡死了,假死,一会儿,运算完了,就又可以拖动了。现在用BackgroundWorker来解决这个问题。
为此,我们新建一个WindowsForm命名为bgwA,拖入一个Label命名为lblPercent,一个ProgressBar命名为pgbPercent,一个Button命名为btnStart。
然后,代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace bgwA { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void BtnStart_Click( object sender, EventArgs e) { BackgroundWorker bgwA = new BackgroundWorker(); bgwA.WorkerReportsProgress = true ; bgwA.DoWork += bgwA_DoWork; bgwA.ProgressChanged += bgwA_ProgressChanged; bgwA.RunWorkerCompleted += bgwA_Completed; bgwA.RunWorkerAsync(); } private void bgwA_DoWork( object sender, DoWorkEventArgs e) { var bgworker = sender as BackgroundWorker; for ( int i = 0; i <= 100; i++) { bgworker.ReportProgress(i); System.Threading.Thread.Sleep(200); } } private void bgwA_ProgressChanged( object sender, ProgressChangedEventArgs e) { this .pgbPercent.Value = e.ProgressPercentage; this .lblPercent.Text = @"已完成:" + e.ProgressPercentage.ToString() + @"%" ; } private void bgwA_Completed( object sender, RunWorkerCompletedEventArgs e) { this .lblPercent.Text = "后台操作结束(可能是程序100%完成,也可能是用户取消或程序异常导致结束)。" ; } } } |
现在运行一下,点击btnStart,进度条跑起来了,再拖动一下程序界面,这下不会没有响应了,不卡,不假死了吧。good.
进阶一:
重复上面说到的注意点:由于DoWork事件内部的代码运行在非UI线程之上,确保在 DoWork 事件处理程序中不操作任何用户界面对象。 而应该通过 ProgressChanged 和 RunWorkerCompleted 事件与用户界面进行通信。
如何在程序运行途中取消正在进行的运算?
要实现在这个功能,我们首先在程序界面上添加一个可以随时中止后台进程的按钮BtnCancel,允许用户在执行过程中取消当前的操作:
与WorkerReportsProgress属性一样,如果要支持取消操作我们需要设置 WorkerSupportsCancellation属性为 true,还要在DoWork方法中进行支持。
接下来对上面的程序进行扩展一下,我们再新建一个WindowsForm命名为bgwB,这次,我们不再通过代码来写BackgroundWorker实例,我们通过从VS的工具箱中直接拉一个BackgroundWorker出来,然后从属性窗口中命名为bgWorker,然后再拖入一个Label命名为lblPercent,一个ProgressBar命名为pgbPercent,一个Button命名为btnStart。
DoWork,ProgressChanged,RunWorkerCompleted三个事件都是通过在bgWorker1属性窗口中双击出来的,下面看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace bgwB { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load( object sender, EventArgs e) { bgWorker1.WorkerReportsProgress = true ; bgWorker1.WorkerSupportsCancellation = true ; } private void BgWorker1_DoWork( object sender, DoWorkEventArgs e) { for ( int i = 0; i <= 100; i++) { if (bgWorker1.CancellationPending == true ) { e.Cancel = true ; break ; } else { bgWorker1.ReportProgress(i); System.Threading.Thread.Sleep(100); } } } private void BgWorker1_ProgressChanged( object sender, ProgressChangedEventArgs e) { this .pgbPercent.Value = e.ProgressPercentage; this .lblPercent.Text = @"已完成:" + e.ProgressPercentage.ToString() + @"%" ; } private void BgWorker1_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e) { this .lblPercent.Text = "后台操作结束(可能是程序100%完成,也可能是用户取消或程序异常导致结束)。" ; } private void BtnStart_Click( object sender, EventArgs e) { bgWorker1.RunWorkerAsync(); } private void BtnCancel_Click( object sender, EventArgs e) { bgWorker1.CancelAsync(); } } } |
现在,我们可以按BtnStart启动后台任务,也可以随时按BtnCancel随时中止后台任务。
发现一个问题没有?在进度条还没跑完100%时,再次按btnStart会怎么样,前后两个例子是不一样,第一个例子中,再次按btnStart,会感觉有两个重叠的进度条在跑,而第二个例子中,则直接抛出了异常:
为什么会这样?第一个例子中,每次按btnStart,都是BackgroundWorker bgwA = new BackgroundWorker();而第二个例子中每次按btnStart,还是那个bgWorker1,只是再次RunWorkerAsync(),解决的方法是先判断一下后台操作是否还在运行中:IsBusy
代码:
1 2 3 4 5 6 7 | private void BtnStart_Click( object sender, EventArgs e) { if (!bgWorker1.IsBusy) { bgWorker1.RunWorkerAsync(); } } |
小结一下:
BackgroundWorker的属性:
IsBusy:获取一个值(true/false),指示 BackgroundWorker 是否正在运行异步操作。
WorkerReportsProgress:获取或设置一个值(true/false),该值指示 BackgroundWorker 能否报告进度更新。
WorkerSupportsCancellation:获取或设置一个值(true/false),该值指示 BackgroundWorker 是否支持异步取消。
BackgroundWorker事件:
DoWork:调用 RunWorkerAsync 时发生。(要在后台进行的耗时操作,不能操作任何用户界面对象。)
ProgressChanged:调用 ReportProgress 时发生。(后台操作进行时,随时向用户界面报告进度,可以操作用户界面对象,如进度条显示,文本显示等。)
RunWorkerCompleted:当后台操作已完成、被取消或引发异常时发生。(可以操作用户界面对象,如显示完成的结果报告。)
进阶二:前后台交互,参数传递
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | private void BgWorker1_DoWork( object sender, DoWorkEventArgs e) { for ( int i = 0; i <= 50; i++) { if (bgWorker1.CancellationPending == true ) { e.Cancel = true ; break ; } else { bgWorker1.ReportProgress(i*2); System.Threading.Thread.Sleep(100); } } } |
看for (int i = 0; i <= 50; i++)以及bgWorker1.ReportProgress(i*2); 原因就自己琢磨一下了。
1 2 3 4 5 6 7 8 | private void BtnStart_Click( object sender, EventArgs e) { if (!bgWorker1.IsBusy) { int intStart = 1000000; bgWorker1.RunWorkerAsync(intStart); } } |
DoWork事件通过参数 e 的Argument属性来获取传递过来的参数。
1 2 3 4 5 6 7 8 | private void BgWorker1_DoWork( object sender, DoWorkEventArgs e) { int intStart = 0; if (e.Argument != null ) { intStart = ( int )e.Argument; } } |
记得传过来的是个object类型的参数,使用时一定不要忘记了转换一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | private void BgWorker1_DoWork( object sender, DoWorkEventArgs e) { int intStart = 0; if (e.Argument != null ) { intStart = ( int )e.Argument; } int intSum = intStart; for ( int i = 0; i <= 50; i++) { if (bgWorker1.CancellationPending == true ) { e.Cancel = true ; break ; } else { intSum += i; bgWorker1.ReportProgress(i*2, "循环求和结果:" +intSum.ToString()); System.Threading.Thread.Sleep(100); } } } private void BgWorker1_ProgressChanged( object sender, ProgressChangedEventArgs e) { this .pgbPercent.Value = e.ProgressPercentage; this .lblPercent.Text = @"已完成:" + e.ProgressPercentage.ToString() + @"%" ; this .txtStatus.Text += e.UserState.ToString() + System.Environment.NewLine; } |
BackgroundWorker的DoWork中ReportProgress加一参数,把需要传出去的参数通过 bgWorker1.ReportProgress(完成百分比, 传出参数) 报告出去,ProgressChanged中把这个传出来的参数显示到UI上:this.txtStatus.Text += e.UserState.ToString() ...。
现在运行一下程序试试看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace bgwB { public partial class Form1 : Form { public Form1() { InitializeComponent(); } string strBgw = "" ; private void Form1_Load( object sender, EventArgs e) { bgWorker1.WorkerReportsProgress = true ; bgWorker1.WorkerSupportsCancellation = true ; } private void BgWorker1_DoWork( object sender, DoWorkEventArgs e) { int intStart = 0; if (e.Argument != null ) { intStart = ( int )e.Argument; } int intSum = intStart; for ( int i = 0; i <= 50; i++) { if (bgWorker1.CancellationPending == true ) { e.Cancel = true ; break ; } else { strBgw = "times:" + i.ToString() + " " + DateTime.Now.ToString(); intSum += i; bgWorker1.ReportProgress(i*2, "循环求和结果:" +intSum.ToString()); System.Threading.Thread.Sleep(100); } } } private void BgWorker1_ProgressChanged( object sender, ProgressChangedEventArgs e) { this .pgbPercent.Value = e.ProgressPercentage; this .lblPercent.Text = @"已完成:" + e.ProgressPercentage.ToString() + @"%" ; this .txtStatus.Text += e.UserState.ToString() + System.Environment.NewLine; this .txtStatus.Text += strBgw + System.Environment.NewLine; } private void BgWorker1_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e) { this .lblPercent.Text = "后台操作结束(可能是程序100%完成,也可能是用户取消或程序异常导致结束)。" ; } private void BtnStart_Click( object sender, EventArgs e) { if (!bgWorker1.IsBusy) { this .txtStatus.Text = "" ; int intStart = 1000000; bgWorker1.RunWorkerAsync(intStart); } } private void BtnCancel_Click( object sender, EventArgs e) { bgWorker1.CancelAsync(); } } } |
结果如图:
总结
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
2016-08-16 C# 使用BackgroundWorker例子及注意点
2016-08-16 C# BackgroundWorker组件学习入门介绍
2016-08-16 浅述WinForm多线程编程与Control.Invoke的应用