BackgroundWorker 线程和支持取消






4.89/5 (108投票s)
使用 BackgroundWorker 线程来提高 UI 的响应能力,支持取消和显示进度。

引言
本文档面向初级和业余开发者,他们认识到应用程序中可以使用多线程,但又不想承担多线程带来的复杂性。多线程是一个许多程序员倾向于避免的概念,因为它可能难以理解、调试和实现。您可以使用 C# 开发一个非常复杂的多线程应用程序。不用担心,即使您不想花时间去理解多线程的所有知识,BackgroundWorker
对象也能让多线程的使用变得简单。
Using the Code
当您的应用程序加载时,它在一个线程上运行。这被称为 UI 线程。通常,这是创建所有 UI 对象并执行所有代码的线程。UI 还使用这个单线程来绘制 UI 对象。因此,当您运行一个耗时任务时,例如在一个目录中处理任意数量的 MP3 文件,您的应用程序就会被锁定,窗口会变白,用户无法点击任何按钮,标题栏会变成“我的酷应用(无响应)”。所以您会回去在 MP3 处理函数中插入一堆 Application.DoEvents()
调用,这样一切都会好起来……并非如此,代码现在运行得更慢了,窗体仍然被锁定,但只是短暂地锁定。整个应用程序看起来有点卡顿。
您需要做的是在另一个线程上执行这种繁重的处理。让 UI 线程空闲以绘制 UI。 .NET 为我们提供了 BackgroundWorker
对象来简化多线程。该对象旨在仅在另一个线程上运行函数,然后在完成时在 UI 线程上调用一个事件。步骤很简单:创建一个 BackgroundWorker
对象,告诉它要在后台线程上运行哪个函数(DoWork
函数),告诉它工作完成后要在 UI 线程上运行哪个函数(RunWorkerCompleted
函数),然后告诉 BackgroundWorker
对象开始工作。
有一个规则需要注意——您不能在未创建它们的线程上访问 UI 对象。因此,如果您在 DoWork
函数中编写了代码行 lblStatus.Text = "正在处理第 5 个文件,共 100 个";
,您将收到一个运行时错误。有两种方法可以解决这个问题,我在示例中都使用了。BackgroundWorker
对象通过提供 ReportProgress
函数来解决这个问题,该函数可以从后台线程的 DoWork
函数调用,这将导致 ProgressChanged
事件在 UI 线程上触发。现在我们可以在 UI 线程上访问 UI 对象并做任何我们想做的事情。但如果我只是想在后台线程上更新一个标签或禁用一个按钮怎么办?没问题,使用 Control.Invoke
,您可以提供一些代码(在匿名函数中)在 UI 线程上运行,我在异步示例中使用此技术来更新进度窗体的标签和进度条。
同步示例
本文档中有两个示例,一个关于同步多线程,另一个关于异步多线程。同步示例的目的是禁用您的应用程序,以便用户无法执行其他操作,但您希望应用程序在通知用户进度并允许用户取消该过程的同时仍然能够绘制。
public partial class fmMain : Form
{
// The progress form will be created and shown modally while the
// synchronous process is running. This form will notify the background
// thread if a cancellation is performed. The background thread
// will update the status label and ProgressBar
// on the Progress Form using Control.Invoke.
private fmProgress m_fmProgress = null;
#region Synchronous BackgroundWorker Thread
private void bnSync_Click( object sender, EventArgs e )
{
// Create a background thread
BackgroundWorker bw = new BackgroundWorker();
bw.DoWork += new DoWorkEventHandler( bw_DoWork );
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler
( bw_RunWorkerCompleted );
// Create a progress form on the UI thread
m_fmProgress = new fmProgress();
// Kick off the Async thread
bw.RunWorkerAsync();
// Lock up the UI with this modal progress form.
m_fmProgress.ShowDialog( this );
m_fmProgress = null;
}
private void bw_DoWork( object sender, DoWorkEventArgs e )
{
// Do some long running task...
int iCount = new Random().Next( 20, 50 );
for( int i = 0; i < iCount; i++ )
{
// The Work to be performed...
Thread.Sleep( 100 );
// Update the description and progress on the modal form
// using Control.Invoke. Invoke will run the anonymous
// function to set the label's text on the UI thread.
// Since it's illegal to touch the UI control on the worker
// thread that we're on right now.
// Moron Anonymous functions:
// https://codeproject.org.cn/books/cs2_anonymous_method.asp
m_fmProgress.lblDescription.Invoke((MethodInvoker) delegate()
{
m_fmProgress.lblDescription.Text =
"Processing file " + i.ToString() +
" of " + iCount.ToString();
m_fmProgress.progressBar1.Value =
Convert.ToInt32( i * ( 100.0 / iCount ) );
});
// Periodically check for a cancellation
// If the user clicks the cancel button, or tries to close
// the progress form, the m_fmProgress.Cancel flag
// will be set to true.
if( m_fmProgress.Cancel )
{
// Set the e.Cancel flag so that the WorkerCompleted event
// knows that the process was cancelled.
e.Cancel = true;
return;
}
}
}
private void bw_RunWorkerCompleted
( object sender, RunWorkerCompletedEventArgs e )
{
// The background process is complete. First we should hide the
// modal Progress Form to unlock the UI. Then we need to inspect our
// response to see if an error occurred, a cancel was requested or
// if we completed successfully.
// Hide the Progress Form
if( m_fmProgress != null )
{
m_fmProgress.Hide();
m_fmProgress = null;
}
// Check to see if an error occurred in the
// background process.
if( e.Error != null )
{
MessageBox.Show( e.Error.Message );
return;
}
// Check to see if the background process was cancelled.
if( e.Cancelled )
{
MessageBox.Show( "Processing cancelled." );
return;
}
// Everything completed normally.
// process the response using e.Result
MessageBox.Show( "Processing is complete." );
}
#endregion
}
异步示例
异步示例的目的是允许用户启动一个进程,同时仍然能够继续使用您的应用程序。但是,用户仍然需要能够确定进程的进度并能够取消它。在此示例中,我们不希望显示模态窗口,因为它会禁用应用程序。
public partial class fmMain : Form
{
// The BackgroundWorker will be used to perform a long running action
// on a background thread. This allows the UI to be free for painting
// as well as other actions the user may want to perform. The background
// thread will use the ReportProgress event to update the ProgressBar
// on the UI thread.
private BackgroundWorker m_AsyncWorker = new BackgroundWorker();
public fmMain()
{
InitializeComponent();
// Create a background worker thread that ReportsProgress &
// SupportsCancellation
// Hook up the appropriate events.
m_AsyncWorker.WorkerReportsProgress = true;
m_AsyncWorker.WorkerSupportsCancellation = true;
m_AsyncWorker.ProgressChanged += new ProgressChangedEventHandler
( bwAsync_ProgressChanged );
m_AsyncWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler
( bwAsync_RunWorkerCompleted );
m_AsyncWorker.DoWork += new DoWorkEventHandler( bwAsync_DoWork );
}
#region Asynchronous BackgroundWorker Thread
private void bnAsync_Click( object sender, EventArgs e )
{
// If the background thread is running then clicking this
// button causes a cancel, otherwise clicking this button
// launches the background thread.
if( m_AsyncWorker.IsBusy )
{
bnAsync.Enabled = false;
lblStatus.Text = "Cancelling...";
// Notify the worker thread that a cancel has been requested.
// The cancel will not actually happen until the thread in the
// DoWork checks the bwAsync.CancellationPending flag, for this
// reason we set the label to "Cancelling...", because we haven't
// actually cancelled yet.
m_AsyncWorker.CancelAsync();
}
else
{
bnAsync.Text = "Cancel";
lblStatus.Text = "Running...";
// Kickoff the worker thread to begin it's DoWork function.
m_AsyncWorker.RunWorkerAsync();
}
}
private void bwAsync_DoWork( object sender, DoWorkEventArgs e )
{
// The sender is the BackgroundWorker object we need it to
// report progress and check for cancellation.
BackgroundWorker bwAsync = sender as BackgroundWorker;
// Do some long running operation here
int iCount = new Random().Next( 20, 50 );
for( int i = 0; i < iCount; i++ )
{
// Working...
Thread.Sleep( 100 );
// Periodically report progress to the main thread so that it can
// update the UI. In most cases you'll just need to send an
// integer that will update a ProgressBar,
// but there is an OverLoad for the ReportProgress function
// so that you can supply some other information
// as well, perhaps a status label?
bwAsync.ReportProgress( Convert.ToInt32( i * ( 100.0 / iCount )));
// Periodically check if a cancellation request is pending.
// If the user clicks cancel the line
// m_AsyncWorker.CancelAsync(); if ran above. This
// sets the CancellationPending to true.
// You must check this flag in here and react to it.
// We react to it by setting e.Cancel to true and leaving.
if( bwAsync.CancellationPending )
{
// Pause for a bit to demonstrate that there is time between
// "Cancelling..." and "Cancel ed".
Thread.Sleep( 1200 );
// Set the e.Cancel flag so that the WorkerCompleted event
// knows that the process was cancelled.
e.Cancel = true;
return;
}
}
bwAsync.ReportProgress( 100 );
}
private void bwAsync_RunWorkerCompleted
( object sender, RunWorkerCompletedEventArgs e )
{
// The background process is complete. We need to inspect
// our response to see if an error occurred, a cancel was
// requested or if we completed successfully.
bnAsync.Text = "Start Long Running Asynchronous Process";
bnAsync.Enabled = true;
// Check to see if an error occurred in the
// background process.
if( e.Error != null )
{
MessageBox.Show( e.Error.Message );
return;
}
// Check to see if the background process was cancelled.
if( e.Cancelled )
{
lblStatus.Text = "Cancelled...";
}
else
{
// Everything completed normally.
// process the response using e.Result
lblStatus.Text = "Completed...";
}
}
private void bwAsync_ProgressChanged
( object sender, ProgressChangedEventArgs e )
{
// This function fires on the UI thread so it's safe to edit
// the UI control directly, no funny business with Control.Invoke.
// Update the progressBar with the integer supplied to us from the
// ReportProgress() function. Note, e.UserState is a "tag" property
// that can be used to send other information from the
// BackgroundThread to the UI thread.
progressBar1.Value = e.ProgressPercentage;
}
#endregion
}
关注点
查看我的其他帖子
关于作者
Andrew D. Weiss
软件工程师
查看我的博客: 更多关于 C# 的内容
