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

如何:简化 WPF 中线程的使用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (31投票s)

2014年4月29日

CPOL

4分钟阅读

viewsIcon

59653

downloadIcon

1631

简化 UI 线程(使用 Dispatcher) 和新线程在 WPF 中的使用

public void TimerCallback(object state)
{
    ThreadInvoker.Instance.RunByUiThread(() =>
    {
        string text = "Timmer Tick: " + DateTime.Now.ToLongTimeString() + "\n";
        var color = Colors.Blue;
        var paragraph = new Paragraph(new Run(text))
                       { Foreground = new SolidColorBrush(color) };
        this.logger.Document.Blocks.Add(paragraph);
    });
} 

引言

本文介绍如何简化 WPF 中线程的使用,包括:

  • UI 线程(使用 Dispatcher)
  • 新线程(使用 Action/Func 调用)

背景

线程的使用非常普遍,线程用于实现应用程序的并行化。多线程使您的应用程序能够执行并发处理,从而一次执行多个操作。您可以在 MSDN 线程教程 中阅读有关线程的更多信息。

我们将讨论分为两种类型的线程:

请注意:我写的是 UI 线程而不是 UI 线程!因为 WPF 中可能存在多个 UI 线程。有关更多信息,请参阅 MSDN 这篇精彩文章中的“多个窗口,多个线程”部分:WPF 线程模型

UI 线程

在 .NET 1.1 时代,我们可以从任何线程访问 UI 对象,而无需首先检查跨线程 UI 更改。这种访问会导致意外的 UI 行为。

但从 .NET 2.0 开始,在尝试从非 UI 线程更改 UI 时会识别这些尝试,并抛出异常。您可以自行承担风险,通过将 CheckForIllegalCrossThreadCalls 标志设置为 false 来避免此检查。有关更多信息,您可以阅读文章:如何处理 GUI 元素的跨线程访问

WPF 使用 Dispatcher 类 来通过 UI 线程调用代码执行。

新线程

.NET 使我们能够以非常基本的方式创建新线程:请参阅 MSDN 文章如何:创建线程

.NET 3.5 及更高版本通过 Action\Func 调用简化了新线程的使用。在本文中,我将通过简化 UI 和新线程的常规线程使用来将其提升到一个新的水平。

简化线程的使用

根据我的经验,最好将线程的使用统一和简化到一个可以在应用程序中使用的结构化实用工具中,原因如下:

  1. 避免了应用程序中的代码重复
  2. 线程调用代码是统一的,易于更改
  3. 方便团队中不那么有经验的程序员访问对象的使用

线程调用器代码

首先,我们为那些只想复制粘贴的人提供线程调用器代码。

public class ThreadInvoker
{
    #region Singleton

    private ThreadInvoker()
    {
    }

    public static ThreadInvoker Instance
    {
        get
        {
            return Nested.instance;
        }
    }

    class Nested
    {
        // Explicit static constructor to tell C# compiler
        // not to mark type as beforefieldinit
        static Nested()
        {
        }
        internal static readonly ThreadInvoker instance = new ThreadInvoker();
    }

    #endregion

    static readonly object padlock = new object();

    #region New Thread

    public void RunByNewThread(Action action)
    {
        lock (padlock)
        {
            action.BeginInvoke(ar => ActionCompleted(ar, res => action.EndInvoke(res)), null);
        }
    }

    public void RunByNewThread<TResult>(Func<TResult> func, Action<TResult> callbackAction)
    {
        lock (padlock)
        {
            func.BeginInvoke(ar => 
            FuncCompleted<TResult>(ar, res => func.EndInvoke(res),callbackAction), null);
        }
    }

    private static void ActionCompleted(IAsyncResult asyncResult,
                                        Action<IAsyncResult> endInvoke)
    {
        if (asyncResult.IsCompleted)
        {
            endInvoke(asyncResult);
        }
    }

    private static void FuncCompleted<TResult>(IAsyncResult asyncResult, 
                                               Func<IAsyncResult, TResult> endInvoke, 
                                               Action<TResult> callbackAction)
    {
        if (asyncResult.IsCompleted)
        {
            TResult response = endInvoke(asyncResult);
            if (callbackAction != null)
            {
                callbackAction(response);
            }
        }
    }

    #endregion

    #region UI Thread

    private Dispatcher m_Dispatcher = null;

    //You have to Init the Dispatcher in the UI thread! 
    // Init once per application (if there is only one Dispatcher).
    public void InitDispacter(Dispatcher dispatcher = null)
    {
        m_Dispatcher = dispatcher == null ? (new UserControl()).Dispatcher : dispatcher;
    }

    public void RunByUiThread(Action action)
    {
        #region UI Thread Safety

        //handle by UI Thread.
        if (m_Dispatcher.Thread != Thread.CurrentThread)
        {
            m_Dispatcher.BeginInvoke(DispatcherPriority.Normal, action);
            return;
        }

        action();

        #endregion
    }

    public T RunByUiThread<T>(Func<T> function)
    {
        #region UI Thread Safety
        //handle by UI Thread.
        if (m_Dispatcher.Thread != Thread.CurrentThread)
        {
            return (T)m_Dispatcher.Invoke(DispatcherPriority.Normal, function);
        }
        return function();
        #endregion
    }

    #endregion
} 

“线程调用器”通过一个简单的单例对象,使我们能够简化线程的使用(使用 Action/Func)。

现在,对于那些真正想理解代码的人,我将分部分解释代码。

在 UI 线程上运行

假设我们想通过 UI 线程运行一段代码。例如,动机是为了需要在非 UI 线程(例如,Timer CallBack)上更新 UI RichTextBox

首先,在使用线程调用器的“在 UI 线程上运行”功能时,我们应该设置 Dispatcher。在大多数情况下,我们每个应用程序可以设置一次,例如在 MainWindow 构造函数中。

public MainWindow()
{
    InitializeComponent();

    //You have to Init the Dispatcher in the UI thread! 
    //init once per application (if there is only one Dispatcher).
    ThreadInvoker.Instance.InitDispacter();

    m_Timer = new Timer(TimerCallback, null, 1000, 1000);
}

如上所示,我们还设置了一个间隔为 1 秒的计时器。每次计时器滴答时,我们都会通过添加计时器滴答时间来更新 UI(RichTextBox )。

protected void TimerCallback(object state)
{
    var numberOfChars = ThreadInvoker.Instance.RunByUiThread(() =>
    {
        string text = "Timmer Tick: " + DateTime.Now.ToLongTimeString() + "\n";

        int res = text.Length;

        var color = Colors.Blue;

        var paragraph = new Paragraph(new Run(text))
                        { Foreground = new SolidColorBrush(color) };

        this.logger.Document.Blocks.Add(paragraph);

        return res;//return number of chars written.
    });

    WriteToLog("Timer callback number of chars written: " + numberOfChars);
}

线程调用器的使用非常简单,只需将代码作为 Action/Func 参数运行在匿名方法中。

在上面的示例中,我使用了匿名方法作为 Func 参数,因为我想要一个返回值。

请注意:如果您使用的是 Func (如上例所示),当前线程将等待结果。但如果您使用的是 Action,当前线程将继续执行,而 Action 将在 UI 线程空闲时执行,正如应该的那样。 :)

让我们深入了解幕后,看看实现。

public T RunByUiThread<T>(Func<T> function)
{
    #region UI Thread Safety

    //handle by UI Thread.
    if (m_Dispatcher.Thread != Thread.CurrentThread)
    {
        return (T)m_Dispatcher.Invoke(DispatcherPriority.Normal, function);
    }

    return function();

    #endregion
}

该方法接收一个泛型 Func ,通过 Dispatcher 检查我们是否在非 UI 线程上运行。如果是,我们使用 Dispatcher 调用 Func ;否则,我们只是运行 Func 。就这么简单。

在新线程上运行

假设我们想通过 新线程运行一段代码。例如,动机是在 Click 事件(即从 UI 线程)中计算时间(当前时间),然后使用 UI 线程在 UI(RichTextBox )中显示时间。

private void logger_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
    ThreadInvoker.Instance.RunByNewThread<int>(() =>
    {
        string text = "Click Time: " + DateTime.Now.ToLongTimeString() + "\n";

        int res = text.Length;

        ThreadInvoker.Instance.RunByUiThread(() =>
        {
            var color = Colors.Red;

            var paragraph = new Paragraph(new Run(text)) 
                            { Foreground = new SolidColorBrush(color) };

            this.logger.Document.Blocks.Add(paragraph);
        });

        return res;

    }, (res) => { WriteToLog("Mouse click number of chars written: " + res); });
}

同样,使用非常简单,只需将代码作为 Action/Func 参数运行在匿名方法中。

在上面的示例中,我再次使用了匿名方法作为 Func 参数,因为我想要一个返回值。

请注意:由于您使用的是新线程,因此这是一个无同步调用!即当前线程将继续执行。如果您使用的是带有返回值的 Func (如上例所示),则可以在回调中处理返回值。

(res) => { WriteToLog("Mouse click number of chars written: " + res); }

让我们深入了解幕后,看看实现。

public void RunByNewThread<TResult>(Func<TResult> func, Action<TResult> callbackAction)
{
    lock (padlock)
    {
        func.BeginInvoke(ar => FuncCompleted<TResult>(ar, res => func.EndInvoke(res),
                         callbackAction), null);
    }
}

private static void FuncCompleted<TResult>(IAsyncResult asyncResult, 
                                           Func<IAsyncResult, TResult> endInvoke, 
                                           Action<TResult> callbackAction)
{
    if (asyncResult.IsCompleted)
    {
        TResult response = endInvoke(asyncResult);

        if (callbackAction != null)
        {
            callbackAction(response);
        }
    }
}

这里稍微复杂一些,因为 Func 在新线程中运行,这是一个无同步活动!获取返回值只能通过使用回调 Action 来实现。

FunCompleted 方法获取返回值并将其发送到回调。回调可以使用匿名 Action 将返回值设置到变量中。

(res) => { WriteToLog("Mouse click number of chars written: " + res); }

因此,代码流程是独立的,程序员无需考虑任何面向线程的编程。

© . All rights reserved.