添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

WPF 开发人员无需编写需要使用多个线程的界面。 由于多线程程序既复杂又难以调试,因此当存在单线程解决方案时,应避免使用多线程程序。

但是,无论构建得多好,没有任何 UI 框架能为每种问题都提供单线程解决方案。 WPF 虽然在这方面有近乎完美的表现,但某些情况下,仍需要使用多线程来改进用户界面 (UI) 响应能力或应用程序性能。 基于上文所述的背景材料,本文对上述情况进行探讨,然后通过对一些低级别的细节进行讨论作出总结。

本主题介绍使用 BeginInvoke 方法进行异步调用的线程处理。 你也可以通过调用 InvokeAsync 方法来执行异步调用,该方法使用 Action Func<TResult> 作为参数。 InvokeAsync 方法返回具有 Task 属性的 DispatcherOperation DispatcherOperation<TResult> 。 可以将 await 关键字与 DispatcherOperation 或相关联的 Task 配合使用。 如果需要同步等待 DispatcherOperation DispatcherOperation<TResult> 返回的 Task ,请调用 DispatcherOperationWait 扩展方法。 调用 Task.Wait 将导致死锁。 有关使用 Task 执行异步操作的详细信息,请参阅 基于任务的异步编程 Invoke 方法还具有以 Action Func<TResult> 为参数的重载。 可使用 Invoke 方法,通过传入 Action Func<TResult> 委托来执行同步调用。

概述和调度程序

通常为 UI。 当 UI 线程接收输入、处理事件、绘制屏幕和运行应用程序代码时,呈现线程通过隐藏方式在后台高效运行。 大多数应用程序使用单个 UI 线程,不过在某些情况下,最好使用多个线程。 我们将稍后通过示例对此进行讨论。

UI 线程在称为 Dispatcher 的对象内对工作项进行排队。 Dispatcher 基于优先级选择工作项,并运行每一个工作项直到完成。 每个 UI 线程必须具有至少一个 Dispatcher ,且每个 Dispatcher 都可精确地在一个线程中执行工作项。

若要生成响应迅速、用户友好的应用程序,诀窍在于通过保持工作项小型化来最大化 Dispatcher 吞吐量。 这样一来,工作项就不会停滞在 Dispatcher 队列中,因等待处理而过时。 输入和响应间任何可察觉的延迟都会让用户不满。

那么,UI 线程如何自由地处理 Dispatcher 队列中的项? 大型操作完成后,它可以将其结果报告回 UI 线程以进行显示。

传统而言,Windows 允许 UI 元素仅由创造它们的线程访问。 这意味着,负责长时间运行任务的后台线程无法在任务完成时更新文本框。 Windows 这么做的目的是确保 UI 组件的完整性。 如果在绘制过程中后台线程更新了列表框的内容,则此列表框看起来可能会很奇怪。

WPF 具有内置互相排斥机制,此机制能强制执行这种协调。 WPF 中的大多数类都派生自 DispatcherObject 。 构造时, DispatcherObject 会存储对 Dispatcher (它链接到当前正在运行的线程)的引用。 实际上, DispatcherObject 与创建它的线程相关联。 在程序执行期间, DispatcherObject 可以调用它的公共 VerifyAccess 方法。 VerifyAccess 检查与当前线程相关联的 Dispatcher ,并将其与构造期间存储的 Dispatcher 引用相比较。 如果它们不匹配, VerifyAccess 会引发异常。 系统会在属于 DispatcherObject 的每个方法的开头调用 VerifyAccess

如果可以修改 UI 的线程只有一个,后台线程将如何与用户进行交互? 后台线程可请求 UI 线程代表自己来执行操作。 它通过向 UI 线程的 Dispatcher 注册工作项来实现此目的。 Dispatcher 类为注册工作项提供两种方法: Invoke BeginInvoke 。 这两种方法都计划一个用于执行的委托。 Invoke 是一个同步调用,也就是说,在 UI 线程真正执行完委托之前,它不会返回。 BeginInvoke 是异步的,它会立即返回。

Dispatcher 按优先级对其队列中的元素排序。 向 Dispatcher 队列添加元素时,可以指定十个级别。 这些优先级均在 DispatcherPriority 枚举中维护。 有关 DispatcherPriority 级别的详细信息可以在 Windows SDK 文档中找到。

实际线程:示例

具有长时间运行计算的单线程应用程序

在等待由响应用户交互而生成的事件时,大多数图形用户界面 (GUI) 在大多数时间处于空闲状态。 通过精心编程,可建设性地使用这些空闲时间,且不会影响 UI 的响应能力。 UI 线程。 这意味着,必须确保定期返回 Dispatcher ,以便在过时之前处理挂起的输入事件。

请考虑以下示例:

这个简单的应用程序从 3 开始向上计数以搜索质数。 用户单击“开始”按钮时,开始执行搜索。 当程序查找到一个质数时,它将根据其发现内容更新用户界面。 用户可随时停止搜索。

尽管十分简单,但对质数的搜索可以永远持续下去,这会带来一些问题。 如果在按钮的单击事件处理程序中处理整个搜索,UI 线程将永远没有机会处理其他事件。 UI 将无法响应输入,也无法处理消息。 它将永远不会重绘,也永远不会响应按钮单击。

可以在单独的线程中搜索质数,但这样的话,我们需要处理一些同步问题。 通过单线程方法,可以直接更新列出所找到的最大质数的标签。

如果将计算任务分解为可管理的多个区块,则可以定期返回 Dispatcher ,并处理事件。 WPF 就有机会重绘和处理输入。

划分计算和事件处理之间的处理时间的最佳方式是从 Dispatcher 管理计算。 通过使用 BeginInvoke 方法,可以在从中绘制 UI 事件的同一队列中计划质数检查。 在我们的示例中,一次仅计划一个质数检查。 完成质数检查后,立即计划下一个检查。 仅当处理挂起的 UI 事件后,此检查才会继续。

Microsoft Word 通过此机制完成拼写检查。 拼写检查是在后台利用 UI 线程的空闲时间完成的。 我们来看一看代码。

下列示例显示了创建用户界面的 XAML。

<Window x:Class="SDKSamples.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Prime Numbers" Width="260" Height="75"
  <StackPanel Orientation="Horizontal" VerticalAlignment="Center" >
    <Button Content="Start"  
            Click="StartOrStop"
            Name="startStopButton"
            Margin="5,0,5,0"
    <TextBlock Margin="10,5,0,0">Biggest Prime Found:</TextBlock>
    <TextBlock Name="bigPrime" Margin="4,5,0,0">3</TextBlock>
  </StackPanel>
</Window>

以下示例显示了代码隐藏。

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Threading;
namespace SDKSamples
    public partial class Window1 : Window
        public delegate void NextPrimeDelegate();
        //Current number to check
        private long num = 3;
        private bool continueCalculating = false;
        public Window1() : base()
            InitializeComponent();
        private void StartOrStop(object sender, EventArgs e)
            if (continueCalculating)
                continueCalculating = false;
                startStopButton.Content = "Resume";
                continueCalculating = true;
                startStopButton.Content = "Stop";
                startStopButton.Dispatcher.BeginInvoke(
                    DispatcherPriority.Normal,
                    new NextPrimeDelegate(CheckNextNumber));
        public void CheckNextNumber()
            // Reset flag.
            NotAPrime = false;
            for (long i = 3; i <= Math.Sqrt(num); i++)
                if (num % i == 0)
                    // Set not a prime flag to true.
                    NotAPrime = true;
                    break;
            // If a prime number.
            if (!NotAPrime)
                bigPrime.Text = num.ToString();
            num += 2;
            if (continueCalculating)
                startStopButton.Dispatcher.BeginInvoke(
                    System.Windows.Threading.DispatcherPriority.SystemIdle,
                    new NextPrimeDelegate(this.CheckNextNumber));
        private bool NotAPrime = false;
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Threading
Imports System.Threading
Namespace SDKSamples
    Partial Public Class MainWindow
        Inherits Window
        Public Delegate Sub NextPrimeDelegate()
        'Current number to check 
        Private num As Long = 3
        Private continueCalculating As Boolean = False
        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub
        Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
            If continueCalculating Then
                continueCalculating = False
                startStopButton.Content = "Resume"
                continueCalculating = True
                startStopButton.Content = "Stop"
                startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
            End If
        End Sub
        Public Sub CheckNextNumber()
            ' Reset flag.
            NotAPrime = False
            For i As Long = 3 To Math.Sqrt(num)
                If num Mod i = 0 Then
                    ' Set not a prime flag to true.
                    NotAPrime = True
                    Exit For
                End If
            ' If a prime number.
            If Not NotAPrime Then
                bigPrime.Text = num.ToString()
            End If
            num += 2
            If continueCalculating Then
                startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
            End If
        End Sub
        Private NotAPrime As Boolean = False
    End Class
End Namespace

以下示例显示了 Button 的事件处理程序。

private void StartOrStop(object sender, EventArgs e)
    if (continueCalculating)
        continueCalculating = false;
        startStopButton.Content = "Resume";
        continueCalculating = true;
        startStopButton.Content = "Stop";
        startStopButton.Dispatcher.BeginInvoke(
            DispatcherPriority.Normal,
            new NextPrimeDelegate(CheckNextNumber));
Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
    If continueCalculating Then
        continueCalculating = False
        startStopButton.Content = "Resume"
        continueCalculating = True
        startStopButton.Content = "Stop"
        startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
    End If
End Sub

除更新 Button 上的文本外,此处理程序还负责通过向 Dispatcher 队列添加委托,计划首个质数检查。 在此事件处理程序完成其工作后一段时间,Dispatcher 将选择执行此委托。

如前文所述,BeginInvoke 是用于计划委托执行的 Dispatcher 成员。 在这种情况下,选择 SystemIdle 优先级。 仅当没有要处理的重要事件时,Dispatcher 才会执行此委托。 UI 响应能力比数字检查更重要。 我们还传递了一个表示数字检查例程的新委托。

public void CheckNextNumber()
    // Reset flag.
    NotAPrime = false;
    for (long i = 3; i <= Math.Sqrt(num); i++)
        if (num % i == 0)
            // Set not a prime flag to true.
            NotAPrime = true;
            break;
    // If a prime number.
    if (!NotAPrime)
        bigPrime.Text = num.ToString();
    num += 2;
    if (continueCalculating)
        startStopButton.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.SystemIdle,
            new NextPrimeDelegate(this.CheckNextNumber));
private bool NotAPrime = false;
Public Sub CheckNextNumber()
    ' Reset flag.
    NotAPrime = False
    For i As Long = 3 To Math.Sqrt(num)
        If num Mod i = 0 Then
            ' Set not a prime flag to true.
            NotAPrime = True
            Exit For
        End If
    ' If a prime number.
    If Not NotAPrime Then
        bigPrime.Text = num.ToString()
    End If
    num += 2
    If continueCalculating Then
        startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
    End If
End Sub
Private NotAPrime As Boolean = False

此方法检查下一个奇数是否是质数。 如果是质数,此方法将直接更新 bigPrimeTextBlock,以反映此发现。 可以如此操作的原因是,该计算发生在用于创建组件的相同线程中。 如果选择使用单独的线程来进行计算,将必须使用更复杂的同步机制,并在 UI 线程中执行更新。 我们将在下一步中演示这种情况。

有关此示例的完整源代码,请参阅具有长时间运行计算的单线程应用程序示例

使用后台线程处理阻塞操作

在图形应用程序中处理阻塞操作可能很困难。 我们不希望从事件处理程序调用阻塞方法,因为应用程序可能看上去冻结。 可以使用单独的线程来处理这些操作,但在完成操作后,必须使用 UI 线程进行同步,因为无法从工作线程直接修改 GUI。 可使用 InvokeBeginInvoke 将委托插入 UI 线程的 Dispatcher。 最终,将通过可修改 UI 元素的权限来执行这些委托。

在本例中,我们模拟了一个检索天气预报的远程过程调用。 我们使用单独的工作线程来执行此调用,并且计划在调用完成时在 UI 线程的 Dispatcher 中更新方法。

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Threading;
namespace SDKSamples
    public partial class Window1 : Window
        // Delegates to be used in placking jobs onto the Dispatcher.
        private delegate void NoArgDelegate();
        private delegate void OneArgDelegate(String arg);
        // Storyboards for the animations.
        private Storyboard showClockFaceStoryboard;
        private Storyboard hideClockFaceStoryboard;
        private Storyboard showWeatherImageStoryboard;
        private Storyboard hideWeatherImageStoryboard;
        public Window1(): base()
            InitializeComponent();
        private void Window_Loaded(object sender, RoutedEventArgs e)
            // Load the storyboard resources.
            showClockFaceStoryboard =
                (Storyboard)this.Resources["ShowClockFaceStoryboard"];
            hideClockFaceStoryboard =
                (Storyboard)this.Resources["HideClockFaceStoryboard"];
            showWeatherImageStoryboard =
                (Storyboard)this.Resources["ShowWeatherImageStoryboard"];
            hideWeatherImageStoryboard =
                (Storyboard)this.Resources["HideWeatherImageStoryboard"];
        private void ForecastButtonHandler(object sender, RoutedEventArgs e)
            // Change the status image and start the rotation animation.
            fetchButton.IsEnabled = false;
            fetchButton.Content = "Contacting Server";
            weatherText.Text = "";
            hideWeatherImageStoryboard.Begin(this);
            // Start fetching the weather forecast asynchronously.
            NoArgDelegate fetcher = new NoArgDelegate(
                this.FetchWeatherFromServer);
            fetcher.BeginInvoke(null, null);
        private void FetchWeatherFromServer()
            // Simulate the delay from network access.
            Thread.Sleep(4000);
            // Tried and true method for weather forecasting - random numbers.
            Random rand = new Random();
            String weather;
            if (rand.Next(2) == 0)
                weather = "rainy";
                weather = "sunny";
            // Schedule the update function in the UI thread.
            tomorrowsWeather.Dispatcher.BeginInvoke(
                System.Windows.Threading.DispatcherPriority.Normal,
                new OneArgDelegate(UpdateUserInterface),
                weather);
        private void UpdateUserInterface(String weather)
            //Set the weather image
            if (weather == "sunny")
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "SunnyImageSource"];
            else if (weather == "rainy")
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "RainingImageSource"];
            //Stop clock animation
            showClockFaceStoryboard.Stop(this);
            hideClockFaceStoryboard.Begin(this);
            //Update UI text
            fetchButton.IsEnabled = true;
            fetchButton.Content = "Fetch Forecast";
            weatherText.Text = weather;
        private void HideClockFaceStoryboard_Completed(object sender,
            EventArgs args)
            showWeatherImageStoryboard.Begin(this);
        private void HideWeatherImageStoryboard_Completed(object sender,
            EventArgs args)
            showClockFaceStoryboard.Begin(this, true);
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Media
Imports System.Windows.Media.Animation
Imports System.Windows.Media.Imaging
Imports System.Windows.Shapes
Imports System.Windows.Threading
Imports System.Threading
Namespace SDKSamples
    Partial Public Class Window1
        Inherits Window
        ' Delegates to be used in placking jobs onto the Dispatcher.
        Private Delegate Sub NoArgDelegate()
        Private Delegate Sub OneArgDelegate(ByVal arg As String)
        ' Storyboards for the animations.
        Private showClockFaceStoryboard As Storyboard
        Private hideClockFaceStoryboard As Storyboard
        Private showWeatherImageStoryboard As Storyboard
        Private hideWeatherImageStoryboard As Storyboard
        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub
        Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
            ' Load the storyboard resources.
            showClockFaceStoryboard = CType(Me.Resources("ShowClockFaceStoryboard"), Storyboard)
            hideClockFaceStoryboard = CType(Me.Resources("HideClockFaceStoryboard"), Storyboard)
            showWeatherImageStoryboard = CType(Me.Resources("ShowWeatherImageStoryboard"), Storyboard)
            hideWeatherImageStoryboard = CType(Me.Resources("HideWeatherImageStoryboard"), Storyboard)
        End Sub
        Private Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
            ' Change the status image and start the rotation animation.
            fetchButton.IsEnabled = False
            fetchButton.Content = "Contacting Server"
            weatherText.Text = ""
            hideWeatherImageStoryboard.Begin(Me)
            ' Start fetching the weather forecast asynchronously.
            Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer)
            fetcher.BeginInvoke(Nothing, Nothing)
        End Sub
        Private Sub FetchWeatherFromServer()
            ' Simulate the delay from network access.
            Thread.Sleep(4000)
            ' Tried and true method for weather forecasting - random numbers.
            Dim rand As New Random()
            Dim weather As String
            If rand.Next(2) = 0 Then
                weather = "rainy"
                weather = "sunny"
            End If
            ' Schedule the update function in the UI thread.
            tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather)
        End Sub
        Private Sub UpdateUserInterface(ByVal weather As String)
            'Set the weather image
            If weather = "sunny" Then
                weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource)
            ElseIf weather = "rainy" Then
                weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource)
            End If
            'Stop clock animation
            showClockFaceStoryboard.Stop(Me)
            hideClockFaceStoryboard.Begin(Me)
            'Update UI text
            fetchButton.IsEnabled = True
            fetchButton.Content = "Fetch Forecast"
            weatherText.Text = weather
        End Sub
        Private Sub HideClockFaceStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
            showWeatherImageStoryboard.Begin(Me)
        End Sub
        Private Sub HideWeatherImageStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
            showClockFaceStoryboard.Begin(Me, True)
        End Sub
    End Class
End Namespace

以下是一些需要注意的详细信息。

  • 创建按钮处理程序

    private void ForecastButtonHandler(object sender, RoutedEventArgs e)
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        hideWeatherImageStoryboard.Begin(this);
        // Start fetching the weather forecast asynchronously.
        NoArgDelegate fetcher = new NoArgDelegate(
            this.FetchWeatherFromServer);
        fetcher.BeginInvoke(null, null);
    
    Private Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        hideWeatherImageStoryboard.Begin(Me)
        ' Start fetching the weather forecast asynchronously.
        Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer)
        fetcher.BeginInvoke(Nothing, Nothing)
    End Sub
    

    单击按钮时,会显示时钟绘图并开始对其进行动画处理。 禁用该按钮。 调用新线程中的 FetchWeatherFromServer 方法,然后返回,这允许 Dispatcher 在我们等待收集天气预报信息时处理事件。

    private void FetchWeatherFromServer()
        // Simulate the delay from network access.
        Thread.Sleep(4000);
        // Tried and true method for weather forecasting - random numbers.
        Random rand = new Random();
        String weather;
        if (rand.Next(2) == 0)
            weather = "rainy";
            weather = "sunny";
        // Schedule the update function in the UI thread.
        tomorrowsWeather.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.Normal,
            new OneArgDelegate(UpdateUserInterface),
            weather);
    
    Private Sub FetchWeatherFromServer()
        ' Simulate the delay from network access.
        Thread.Sleep(4000)
        ' Tried and true method for weather forecasting - random numbers.
        Dim rand As New Random()
        Dim weather As String
        If rand.Next(2) = 0 Then
            weather = "rainy"
            weather = "sunny"
        End If
        ' Schedule the update function in the UI thread.
        tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather)
    End Sub
    

    为简便起见,本例中没有任何网络代码。 通过使新线程进入休眠状态四秒钟,模拟网络访问的延迟。 此时,原始 UI 线程仍在运行并对事件作出响应。 为了对此进行演示,我们让动画保持运行状态,最小化和最大化按钮也继续工作。

    延迟结束且我们随机选择了天气预报后,就可以报告回 UI 线程。 若要执行此操作,需要使用 UI 线程的 Dispatcher,在该线程中计划对 UpdateUserInterface 的调用。 将描述天气的字符串传递给此计划方法调用。

  • 更新 UI

    private void UpdateUserInterface(String weather)
        //Set the weather image
        if (weather == "sunny")
            weatherIndicatorImage.Source = (ImageSource)this.Resources[
                "SunnyImageSource"];
        else if (weather == "rainy")
            weatherIndicatorImage.Source = (ImageSource)this.Resources[
                "RainingImageSource"];
        //Stop clock animation
        showClockFaceStoryboard.Stop(this);
        hideClockFaceStoryboard.Begin(this);
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;
    
    Private Sub UpdateUserInterface(ByVal weather As String)
        'Set the weather image
        If weather = "sunny" Then
            weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource)
        ElseIf weather = "rainy" Then
            weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource)
        End If
        'Stop clock animation
        showClockFaceStoryboard.Stop(Me)
        hideClockFaceStoryboard.Begin(Me)
        'Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weather
    End Sub
    

    当 UI 线程中的 Dispatcher 有空时,它将执行计划的对 UpdateUserInterface 的调用。 此方法停止时钟动画,并选择一张映像用于描述天气。 它将显示此映像,并还原“获取预报”按钮。

    多窗口、多线程

    某些 WPF 应用程序要求多个顶层窗口。 通过单个线程/Dispatcher 组合来管理多个窗口是完全可以接受的,但有时多线程可以做得更好。 尤其当这些窗口中的某一个将有可能要独占线程时,更是如此。

    Windows 资源管理器以这种方式工作。 每个新资源管理器窗口都属于原始进程,但它是在独立线程的控件下创建的。

    通过使用 WPFFrame 控件可以显示网页。 我们可以轻松创建一个简单的 Internet Explorer 替代项。 让我们从一个重要功能开始:打开新资源管理器窗口的能力。 当用户单击“新建窗口”按钮时,我们将在单独的线程中启动窗口的副本。 这样一来,在其中一个窗口中的长时间运行或阻塞操作将不会锁定其他窗口。

    在实际情况下,Web 浏览器模型自身拥有复杂的线程模型。 由于大多数读者都熟悉它,所以我们选择它。

    以下示例显示了代码。

    <Window x:Class="SDKSamples.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MultiBrowse"
        Height="600" 
        Width="800"
        Loaded="OnLoaded"
      <StackPanel Name="Stack" Orientation="Vertical">
        <StackPanel Orientation="Horizontal">
          <Button Content="New Window"
                  Click="NewWindowHandler" />
          <TextBox Name="newLocation"
                   Width="500" />
          <Button Content="GO!"
                  Click="Browse" />
        </StackPanel>
        <Frame Name="placeHolder"
                Width="800"
                Height="550"></Frame>
      </StackPanel>
    </Window>
    
    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Threading;
    using System.Threading;
    namespace SDKSamples
        public partial class Window1 : Window
            public Window1() : base()
                InitializeComponent();
            private void OnLoaded(object sender, RoutedEventArgs e)
               placeHolder.Source = new Uri("http://www.msn.com");
            private void Browse(object sender, RoutedEventArgs e)
                placeHolder.Source = new Uri(newLocation.Text);
            private void NewWindowHandler(object sender, RoutedEventArgs e)
                Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
                newWindowThread.SetApartmentState(ApartmentState.STA);
                newWindowThread.IsBackground = true;
                newWindowThread.Start();
            private void ThreadStartingPoint()
                Window1 tempWindow = new Window1();
                tempWindow.Show();
                System.Windows.Threading.Dispatcher.Run();
    Imports System.Windows
    Imports System.Windows.Controls
    Imports System.Windows.Data
    Imports System.Windows.Threading
    Imports System.Threading
    Namespace SDKSamples
        Partial Public Class Window1
            Inherits Window
            Public Sub New()
                MyBase.New()
                InitializeComponent()
            End Sub
            Private Sub OnLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
               placeHolder.Source = New Uri("http://www.msn.com")
            End Sub
            Private Sub Browse(ByVal sender As Object, ByVal e As RoutedEventArgs)
                placeHolder.Source = New Uri(newLocation.Text)
            End Sub
            Private Sub NewWindowHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
                Dim newWindowThread As New Thread(New ThreadStart(AddressOf ThreadStartingPoint))
                newWindowThread.SetApartmentState(ApartmentState.STA)
                newWindowThread.IsBackground = True
                newWindowThread.Start()
            End Sub
            Private Sub ThreadStartingPoint()
                Dim tempWindow As New Window1()
                tempWindow.Show()
                System.Windows.Threading.Dispatcher.Run()
            End Sub
        End Class
    End Namespace
    

    此代码中的以下线程段对我们来说是最有趣的:

    private void NewWindowHandler(object sender, RoutedEventArgs e)
        Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
        newWindowThread.SetApartmentState(ApartmentState.STA);
        newWindowThread.IsBackground = true;
        newWindowThread.Start();
    
    Private Sub NewWindowHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Dim newWindowThread As New Thread(New ThreadStart(AddressOf ThreadStartingPoint))
        newWindowThread.SetApartmentState(ApartmentState.STA)
        newWindowThread.IsBackground = True
        newWindowThread.Start()
    End Sub
    

    当单击“新建窗口”按钮时,将调用该方法。 它创建了一个新线程,并以异步方式启动。

    private void ThreadStartingPoint()
        Window1 tempWindow = new Window1();
        tempWindow.Show();
        System.Windows.Threading.Dispatcher.Run();
    
    Private Sub ThreadStartingPoint()
        Dim tempWindow As New Window1()
        tempWindow.Show()
        System.Windows.Threading.Dispatcher.Run()
    End Sub
    

    此方法是新线程的起点。 我们在此线程的控件下创建了一个新窗口。 WPF 自动创建新的 Dispatcher 来管理新线程。 若要使窗口功能化,我们要做的是启动 Dispatcher

    技术详细信息和疑难点

    使用线程处理编写组件

    Microsoft .NET Framework 开发人员指南介绍了组件如何向其客户端公开异步行为的模式(请参阅基于事件的异步模式概述)。 例如,假设我们要将 FetchWeatherFromServer 方法打包到可重用的非图形组件。 遵循标准 Microsoft .NET Framework 模式,它将如下所示。

    public class WeatherComponent : Component
        //gets weather: Synchronous
        public string GetWeather()
            string weather = "";
            //predict the weather
            return weather;
        //get weather: Asynchronous
        public void GetWeatherAsync()
            //get the weather
        public event GetWeatherCompletedEventHandler GetWeatherCompleted;
    public class GetWeatherCompletedEventArgs : AsyncCompletedEventArgs
        public GetWeatherCompletedEventArgs(Exception error, bool canceled,
            object userState, string weather)
            base(error, canceled, userState)
            _weather = weather;
        public string Weather
            get { return _weather; }
        private string _weather;
    public delegate void GetWeatherCompletedEventHandler(object sender,
        GetWeatherCompletedEventArgs e);
    
    Public Class WeatherComponent
        Inherits Component
        'gets weather: Synchronous 
        Public Function GetWeather() As String
            Dim weather As String = ""
            'predict the weather
            Return weather
        End Function
        'get weather: Asynchronous 
        Public Sub GetWeatherAsync()
            'get the weather
        End Sub
        Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
    End Class
    Public Class GetWeatherCompletedEventArgs
        Inherits AsyncCompletedEventArgs
        Public Sub New(ByVal [error] As Exception, ByVal canceled As Boolean, ByVal userState As Object, ByVal weather As String)
            MyBase.New([error], canceled, userState)
            _weather = weather
        End Sub
        Public ReadOnly Property Weather() As String
                Return _weather
            End Get
        End Property
        Private _weather As String
    End Class
    Public Delegate Sub GetWeatherCompletedEventHandler(ByVal sender As Object, ByVal e As GetWeatherCompletedEventArgs)
    

    GetWeatherAsync 将使用上述的技术之一(如创建后台线程)来以异步方式工作,而非阻止调用线程。

    此模式最重要的部分之一是在调用 MethodNameAsync 方法开始工作的同一线程上调用 MethodNameCompleted 方法。 可通过存储 CurrentDispatcher 使用 WPF 轻松完成此操作,但非图形组件只能在 WPF 应用程序中使用,不能在 Windows 窗体或 ASP.NET 程序中使用。

    DispatcherSynchronizationContext 类可满足此需求 - 将其视作与其他 UI 框架配合使用的 Dispatcher 的简化版本。

    public class WeatherComponent2 : Component
        public string GetWeather()
            return fetchWeatherFromServer();
        private DispatcherSynchronizationContext requestingContext = null;
        public void GetWeatherAsync()
            if (requestingContext != null)
                throw new InvalidOperationException("This component can only handle 1 async request at a time");
            requestingContext = (DispatcherSynchronizationContext)DispatcherSynchronizationContext.Current;
            NoArgDelegate fetcher = new NoArgDelegate(this.fetchWeatherFromServer);
            // Launch thread
            fetcher.BeginInvoke(null, null);
        private void RaiseEvent(GetWeatherCompletedEventArgs e)
            if (GetWeatherCompleted != null)
                GetWeatherCompleted(this, e);
        private string fetchWeatherFromServer()
            // do stuff
            string weather = "";
            GetWeatherCompletedEventArgs e =
                new GetWeatherCompletedEventArgs(null, false, null, weather);
            SendOrPostCallback callback = new SendOrPostCallback(DoEvent);
            requestingContext.Post(callback, e);
            requestingContext = null;
            return e.Weather;
        private void DoEvent(object e)
            //do stuff
        public event GetWeatherCompletedEventHandler GetWeatherCompleted;
        public delegate string NoArgDelegate();
    
    Public Class WeatherComponent2
        Inherits Component
        Public Function GetWeather() As String
            Return fetchWeatherFromServer()
        End Function
        Private requestingContext As DispatcherSynchronizationContext = Nothing
        Public Sub GetWeatherAsync()
            If requestingContext IsNot Nothing Then
                Throw New InvalidOperationException("This component can only handle 1 async request at a time")
            End If
            requestingContext = CType(DispatcherSynchronizationContext.Current, DispatcherSynchronizationContext)
            Dim fetcher As New NoArgDelegate(AddressOf Me.fetchWeatherFromServer)
            ' Launch thread
            fetcher.BeginInvoke(Nothing, Nothing)
        End Sub
        Private Sub [RaiseEvent](ByVal e As GetWeatherCompletedEventArgs)
            RaiseEvent GetWeatherCompleted(Me, e)
        End Sub
        Private Function fetchWeatherFromServer() As String
            ' do stuff
            Dim weather As String = ""
            Dim e As New GetWeatherCompletedEventArgs(Nothing, False, Nothing, weather)
            Dim callback As New SendOrPostCallback(AddressOf DoEvent)
            requestingContext.Post(callback, e)
            requestingContext = Nothing
            Return e.Weather
        End Function
        Private Sub DoEvent(ByVal e As Object)
            'do stuff
        End Sub
        Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
        Public Delegate Function NoArgDelegate() As String
    End Class
    

    有时无法完全锁定 UI 线程。 让我们考虑一下 MessageBox 类的 Show 方法。 在用户单击“确定”按钮之前,Show 不会返回。 但是,它却会创建一个窗口,该窗口为了获得交互性而必须具有消息循环。 在等待用户单击“确定”时,原始应用程序窗口将不会响应用户的输入。 但是,它将继续处理绘制消息。 当被覆盖和被显示时,原始窗口将重绘其本身。

    一些线程必须负责消息框窗口。 WPF 可以为消息框窗口创建新线程,但此线程无法在原始窗口中绘制禁用的元素(请回忆之前所讨论的互相排斥)。 WPF 使用嵌套消息处理系统。 Dispatcher 类包括一个名为 PushFrame 的特殊方法,它存储应用程序的当前执行点,然后启动一个新的消息循环。 当嵌套消息循环结束后,将在原始 PushFrame 调用之后继续执行。

    在此情况下,PushFrame 将在调用 MessageBox.Show 时维护程序上下文,并且它将启动一个新的消息循环,用于重绘后台窗口,并处理对消息框窗口的输入。 当用户单击“确定”并清除弹出窗口时,嵌套循环将退出,并在调用 Show 后继续控制。

    过时的路由事件

    引发事件时,WPF 中的路由事件系统会通知整个树。

    <Canvas MouseLeftButtonDown="handler1" 
            Width="100"
            Height="100"
      <Ellipse Width="50"
               Height="50"
               Fill="Blue" 
               Canvas.Left="30"
               Canvas.Top="50" 
               MouseLeftButtonDown="handler2"
    </Canvas>
    

    在椭圆形上按下鼠标左键时,将执行 handler2handler2 完成后,事件将传递到 Canvas 对象,后者使用 handler1 对其进行处理。 仅当 handler2 没有显式标记事件对象为已处理时,才会发生这种情况。

    handler2 有可能花费大量时间来处理此事件。 handler2 有可能使用 PushFrame 来启动嵌套消息循环,并在数小时内不会返回任何内容。 如果在此消息循环完成时,handler2 尚未将事件标记为已处理,该事件将沿树向上传递(即使它很旧)。

    重新进入和锁定

    公共语言运行时 (CLR) 的锁定机制与人们所设想的完全不同;可能有人以为在请求锁定时,线程将完全停止操作。 实际上,该线程将继续接收和处理高优先级的消息。 这样有助于防止死锁,并使接口最低限度地响应,但这样做有可能引入细微 bug。 绝大多数时间里,你无需知晓有关这点的任何情况,但在极少数情况下(通常涉及 Win32 窗口消息或 COM STA 组件),可能需要知道这一点。

    大部分接口在生成过程中并未考虑线程安全问题,这是因为开发人员在开发过程中假定 UI 绝不会由一个以上的线程访问。 在此情况下,该单个线程可能在意外情况下更改环境,造成不良影响,这些影响应由 DispatcherObject 互相排斥机制来解决。 请看下面的伪代码:

    大多数情况下这都没有问题,但在某些时候 WPF 中的异常重入确实会造成严重问题。 因此在某些关键时刻,WPF 调用 DisableProcessing,这会更改该线程的锁定指令,以使用 WPF 无重入锁定,而非常规 CLR 锁定。

    那么,为何 CLR 团队选择这种行为? 它与 COM STA 对象和完成线程有关。 在对一个对象进行垃圾回收时,其 Finalize 方法运行在专用终结器线程之上,而非 UI 线程上。 这其中就存在问题,因为在 UI 线程上创建的 COM STA 对象只能在 UI 线程上释放。 CLR 相当于 BeginInvoke(在此例中使用 Win32 的 SendMessage)。 但如果 UI 线程正忙,终结器线程被停止,COM STA 对象无法被释放,这将造成严重的内存泄漏。 因此,CLR 团队通过严格的调用,使锁定以这种方式工作。

    WPF 的任务是在不重新引入内存泄漏的情况下,避免异常的重入,因此我们不阻止各个位置的重入。

  • 具有长时间运行计算的单线程应用程序示例
  •