65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (15投票s)

2014年4月23日

CPOL

3分钟阅读

viewsIcon

32234

downloadIcon

897

编写具有响应式 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 日:初始版本
© . All rights reserved.