简单的任务并行库 (TPL) 示例






4.92/5 (15投票s)
编写具有响应式 UI 的多线程应用程序。
引言
有时,我需要生成后台任务,同时保持 UI 响应。有很多方法可以做到这一点。这个特定的例子适用于 .NET 4.0 的任务并行库。
背景
想象一下,有一个程序必须加载一个大文件,解析其内容,并更新 UI。您希望 UI 具有响应性,并让加载在后台进行。当然,还有很多其他例子,您只是希望程序自己去完成任务,让用户安静地做其他事情,而不是坐在那里等待沙漏光标或“请等待”弹出窗口。
Using the Code
项目中的代码有很好的注释,理解它的最简单方法是在 IDE 中打开它。这段文字只是一个简短的概述。
线程类 HTTPMgr
worker 类 HTTPMgr
是如何并发执行多个任务,从而允许用户拥有响应式 UI 的一个例子。
类级别声明
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// The object used for cancelling tasks.
/// </summary>
private CancellationTokenSource m_CancelTokenSource = null;
/// <summary>
/// The list of created tasks.
/// </summary>
private List<Task> m_TaskList = null;
m_CancelTokenSource
被声明为类级别,因此如果需要,它可以用于取消正在运行的任务。每个 Task
实例都有一个引用保存在 m_TaskList
中,以便可以在 FetchPages()
方法之外对其进行操作。
FetchPages() 方法的核心
// First we need a source object for the cancellation token.
m_CancelTokenSource = new CancellationTokenSource();
// Get a reference to the cancellation token.
CancellationToken ReadFileCancelToken = m_CancelTokenSource.Token;
// I am iterating through the list of URLs to use one task per URL to do them in parallel.
for (Int32 OpIterator = 0; OpIterator < m_HostsToCheck.Count; OpIterator++)
{
// Make a new variable for use in the thread since OpIterator is in the main thread.
// One might think it doesn't make a difference, but it does.
Int32 InnerIterator = OpIterator;
Task NewTask = Task.Factory.StartNew(() =>
{
// If cancel has been chosen, throw an exception now before doing anything.
ReadFileCancelToken.ThrowIfCancellationRequested();
try
{
// ... do some stuff here
Int32 ByteCounter = 0;
do
{
// Since we are in as ParallelLoopResult, checked for as "Cancel" again at each
// iteration of the loop. If cancellation is requested, then make sure
// the cancellation exception is thrown.
if (ReadFileCancelToken.IsCancellationRequested)
{
ReadFileCancelToken.ThrowIfCancellationRequested();
}
// ... do some other stuff in a loop here.
}
while (ByteCounter > 0); // any more data to read?
// Print out page source
// Call back with the text for the web page and a name. InnerIterator is used
// in this example to tell the caller which WebBrowser instance to use.
RaiseOnChange(ReturnStatus.InformationOnly, InnerIterator, PageData, PageInfo);
} // END try
catch (OperationCanceledException exCancel)
{
// This is the exception raised on a Cancel. It is not really an error,
// but a planned exception. So we tell the caller.
RaiseOnChange(ReturnStatus.Failure, InnerIterator, "", null);
} // END catch (OperationCanceledException exCancel)
catch (Exception exUnhandled)
{
// Debug logging for this example project.
// Replace it with your logging, if you want.
Shared.WriteToDebugFile(exUnhandled);
} // END catch (Exception exUnhandled)
finally
{
// Clean up resources.
} // END finally
}, ReadFileCancelToken);
// The task is now defined and started,
// so the object referencing the task is now added to the list.
m_TaskList.Add(NewTask);
} // END for (int OpIterator = 0; OpIterator < 100; OpIterator++)
如何取消正在运行的任务
/// <summary>
/// Cancels whatever tasks are running.
/// </summary>
public void CancelRunningTasks()
{
try
{
// To cancel tasks, we must have:
// 1 - An object referencing the cancellation token
// 2 - A list of tasks
if ((m_CancelTokenSource != null) && (m_TaskList != null))
{
// 3 - And, of course, we need tasks to cancel. :)
if (m_TaskList.Count > 0)
{
// Log it for your convenience.
Shared.WriteToDebugFile("Cancelling tasks.");
// Since all tasks reference this cancellation token, just tell the
// token reference to cancel. As each task executes, this is checked.
m_CancelTokenSource.Cancel();
// Now just wait for the tasks to finish.
Task.WaitAll(m_TaskList.ToArray<Task>());
}
else
{
Shared.WriteToDebugFile("Cancel tasks requested, but no tasks to cancel.");
}
}
else
{
Shared.WriteToDebugFile("Cancel tasks requested,
but no token to use or no task list was found.");
}
} // END try
catch (Exception ex)
{
// Debug logging for this example project.
// Replace it with your logging, if you want.
Shared.WriteToDebugFile(ex);
} // END catch (Exception ex)
finally
{
} // END finally
} // END public void CancelRunningTasks()
还剩下多少任务?
/// <summary>
/// Returns the number of tasks left to complete,
/// and by out parameters, other counts of task states.
/// </summary>
/// <param name="TasksCompleted">The # of tasks that are run and done.</param>
/// <param name="TasksCanceled">The # of tasks that were cancelled.</param>
/// <param name="TasksFaulted">The # of tasks that reported a faulted state.</param>
/// <param name="TasksInWaiting">The # of tasks in some waiting state.</param>
/// <returns>The # of tasks either running or waiting to run.</returns>
public Int32 TasksRemaining(out Int32 TasksCompleted,
out Int32 TasksCanceled,
out Int32 TasksFaulted,
out Int32 TasksInWaiting)
{
DateTime dtmMethodStart = DateTime.Now;
Int32 NumTasksRemaining = 0;
TasksCompleted = 0;
TasksCanceled = 0;
TasksFaulted = 0;
TasksInWaiting = 0;
try
{
if ((m_CancelTokenSource != null) && (m_TaskList != null))
{
if (m_TaskList.Count > 0)
{
foreach (Task ATask in m_TaskList)
{
if (ATask != null)
{
if (ATask.IsCompleted)
{
TasksCompleted += 1;
} // END if (ATask.IsCompleted)
if (ATask.IsCanceled)
{
TasksCanceled += 1;
} // END if (ATask.IsCompleted)
if (ATask.IsFaulted)
{
TasksFaulted += 1;
} // END if (ATask.IsFaulted)
if ((ATask.Status == TaskStatus.Running) ||
(ATask.Status == TaskStatus.WaitingForActivation) ||
(ATask.Status == TaskStatus.WaitingForChildrenToComplete) ||
(ATask.Status == TaskStatus.WaitingToRun))
{
NumTasksRemaining += 1;
} // END if ((ATask.Status == TaskStatus.Running) ||
// (ATask.Status == TaskStatus.WaitingForActivation) ||
// (ATask.Status == TaskStatus.WaitingForChildrenToComplete) ||
// (ATask.Status == TaskStatus.WaitingToRun))
if ((ATask.Status == TaskStatus.WaitingForActivation) ||
(ATask.Status == TaskStatus.WaitingForChildrenToComplete) ||
(ATask.Status == TaskStatus.WaitingToRun))
{
TasksInWaiting += 1;
} // END if ((ATask.Status == TaskStatus.WaitingForActivation) ||
// (ATask.Status == TaskStatus.WaitingForChildrenToComplete) ||
// (ATask.Status == TaskStatus.WaitingToRun))
} // END if (ATask != null)
} // END foreach (Task ATask in m_TaskList)
} // END if (m_TaskList.Count > 0)
} // END if ((m_CancelTokenSource != null) && (m_TaskList != null))
} // END try
catch (Exception ex)
{
// Debug logging for this example project.
// Replace it with your logging, if you want.
Shared.WriteToDebugFile(ex);
} // END catch (Exception exUnhandled)
finally
{
} // END finally
return NumTasksRemaining;
} // END public Int32 TasksRemaining(out Int32 TasksCompleted,
// out Int32 TasksCanceled, out Int32 TasksFaulted, out Int32 TasksInWaiting)
使用线程类 (MainForm.cs)
UI,在本例中是 *MainForm.cs*,实例化 HTTPMgr
并使用它。 HTTPMgr
向 *MainForm.cs* 引发事件以指示状态,并且 *MainForm.cs* 使用计时器来检查还剩下多少任务。当 HTTPMgr
实例正在运行时,该窗体仍然可用,因为这些任务在后台执行。
有人可能会问,在一个线程中引发事件回到 UI 线程是否有问题?如果您注意到 *MainForm.cs* 中委托给事件的方法,则使用 "InvokeRequired
" 和 "BeginInvoke
" 来确保一个线程不会跨越到另一个线程。
MainForm.cs 声明
/// <summary>
/// This is the object that handles the HTTP page loading. It is module-level so that
/// the callback can be handled outside the calling method.
/// </summary>
HTTPMgr m_WorkerObject = null;
启动 HTTPMgr 对象
此代码实例化该对象,使用委托将事件连接到窗体中的本地方法,并启动该方法以在后台执行某些操作(在本例中,加载一堆网页)。
m_WorkerObject = new HTTPMgr();
m_WorkerObject.OnChange += new HTTPMgr.ChangeDelegate(WorkerObject_OnChange);
m_WorkerObject.FetchPages();
从线程操作接收信息
此代码显示了如何在线程运行时从中传输信息。您也可以让线程/任务对您稍后取回的对象进行操作。
/// <summary>
/// This is the method called by the callback of whatever task is trying
/// to pass back information.
/// A check is made so that the callback thread does not try to access objects
/// that exist in the UI thread.
/// </summary>
/// <param name="MsgType"></param>
/// <param name="Index"></param>
/// <param name="Message"></param>
/// <param name="AdditionalInfo"></param>
private void WorkerObject_OnChange(HTTPMgr.ReturnStatus MsgType,
Int32 Index, String Message, List<String> AdditionalInfo)
{
if (this.InvokeRequired)
{
// This callback is in the caller's thread, which is not the UI thread.
// So we tell the form to call this method using the data provided in the UI thread.
// The call goes on a stack, and is executed in the UI thread.
this.BeginInvoke(new HTTPMgr.ChangeDelegate(WorkerObject_OnChange),
MsgType, Index, Message, AdditionalInfo);
}
else
{
// If we are here, we are in the UI thread.
try
{
// ... do some UI stuff.
} // END try
catch (Exception ex)
{
// Debug logging for this example project.
// Replace it with your logging, if you want.
Shared.WriteToDebugFile(ex);
}
} // END else of [if (this.InvokeRequired)]
} // END private void WorkerObject_OnChange(HTTPMgr.ReturnStatus MsgType,
// String Message, Dictionary<String, Object> AdditionalInfo)
检查异步 HTTPMgr FetchPages() 进程的状态
为了密切关注进度,我使用了一个计时器从 HTTPMgr
实例获取任务状态。
/// <summary>
/// The timer is used to monitor the status of the tasks.
/// Once there are no tasks remaining, the Start button is restored to showing Start.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PageTimer_Tick(object sender, EventArgs e)
{
try
{
if (m_WorkerObject != null)
{
// The method requires these as out parameters, but they are not used here.
// They were put in the method for illustrative purposes,
// not that they are necessary.
Int32 TasksCompleted = 0;
Int32 TasksCanceled = 0;
Int32 TasksFaulted = 0;
Int32 TasksInWaiting = 0;
Int32 TasksRemaining = 0;
Int32 TasksRunning = 0;
TasksRemaining = m_WorkerObject.TasksRemaining(out TasksCompleted,
out TasksCanceled,
out TasksFaulted,
out TasksInWaiting);
TasksRunning = TasksRemaining - TasksInWaiting;
TaskRunningLabel.Text = String.Format
(TaskRunningLabel.Tag.ToString(), TasksRunning.ToString());
TasksCanceledLabel.Text = String.Format
(TasksCanceledLabel.Tag.ToString(), TasksCanceled.ToString());
TasksCompletedLabel.Text = String.Format
(TasksCompletedLabel.Tag.ToString(), TasksCompleted.ToString());
TasksFaultedLabel.Text = String.Format
(TasksFaultedLabel.Tag.ToString(), TasksFaulted.ToString());
TasksWaitingLabel.Text = String.Format
(TasksWaitingLabel.Tag.ToString(), TasksInWaiting.ToString());
// If Cancelling (button shows Stopping), we do not need to do anything.
// Most likely, this event is not going to fire under that condition.
if (StartButton.Text != "Stopping")
{
if (TasksRemaining == 0)
{
StartButton.Text = "Start";
StartButton.Enabled = true;
PageTimer.Enabled = false;
m_WorkerObject.OnChange -= WorkerObject_OnChange;
m_WorkerObject.Dispose();
m_WorkerObject = null;
} // END if (m_WorkerObject.TasksRemaining
// (out TasksCompleted, out TasksCanceled,
// out TasksFaulted, out TasksInWaiting) == 0)
} // END if (StartButton.Text != "Stopping")
} // END if (m_WorkerObject != null)
} // END try
catch (Exception ex)
{
// Debug logging for this example project.
// Replace it with your logging, if you want.
Shared.WriteToDebugFile(ex);
}
} // END private void PageTimer_Tick(object sender, EventArgs e)
好了,差不多就是这样了。任何占用您 UI 并且可以在后台运行的功能都可以通过这种方式处理。应用程序对用户越有响应,他们就越喜欢它,并且您看起来就越专业。
关注点
用户通常似乎很惊讶他们可以使用 UI 做事情,并且在等待长时间运行的任务完成时它不会冻结。
历史
- 2014 年 4 月 23 日:初始版本