共用方式為


執行緒模型

Windows Presentation Foundation (WPF) 的設計目的是要節省開發人員執行緒的困難。 因此,大部分的 WPF 開發人員不會撰寫使用多個執行緒的介面。 由於多執行緒的程式非常複雜且很難偵錯,因此,若有單一執行緒解決方案,就應避免使用多執行緒程式。

不過,無論架構有多好,任何 UI 架構都無法針對各種問題提供單一執行緒解決方案。 WPF 已接近尾聲,但仍有多個執行緒改善使用者介面 (UI) 回應性或應用程式效能的情況。 討論一些背景資料之後,本文會探索其中一些情況,然後以一些較低層級的詳細資料討論結束。

注意

本主題討論使用 InvokeAsync 方法進行非同步呼叫的執行緒。 方法 InvokeAsync 會採用 ActionFunc<TResult> 作為參數,並傳 DispatcherOperation 回 具有 屬性的 TaskDispatcherOperation<TResult> 。 您可以使用 await 關鍵字搭配 DispatcherOperation 或相關聯的 Task 。 如果您需要同步 Task 等候 或 DispatcherOperation<TResult>DispatcherOperation 傳回的 ,請呼叫 DispatcherOperationWait 擴充方法。 呼叫 Task.Wait 會導致 死結 。 如需使用 Task 來執行非同步作業的詳細資訊,請參閱 以工作為基礎的非同步程式設計

若要進行同步呼叫,請使用 Invoke 方法,此方法也具有採用委派、 ActionFunc<TResult> 參數的多載。

概觀和發送器

一般而言,WPF 應用程式會以兩個執行緒開頭:一個用於處理轉譯,另一個用於管理 UI。 轉譯執行緒會在 UI 執行緒接收輸入、處理事件、繪製畫面及執行應用程式程式碼時,有效地在背景中執行隱藏。 大部分的應用程式都會使用單一 UI 執行緒,但在某些情況下,最好使用數個執行緒。 我們稍後會用範例討論這個問題。

UI 執行緒會將物件內的工作專案排入佇列, Dispatcher 稱為 。 Dispatcher 會依優先權選取工作項目,並逐一執行以完成每個工作項目。 每個 UI 執行緒必須至少有一個 Dispatcher ,而且每個 Dispatcher 執行緒都可以在一個執行緒中執行工作專案。

建置回應式使用者易記應用程式的訣竅,是藉由讓工作專案保持較小來最大化 Dispatcher 輸送量。 如此一來,專案就永遠不會過時坐在佇列中 Dispatcher 等待處理。 輸入與回應之間任何可察覺到的延遲都能讓使用者感到挫折。

WPF 應用程式應該如何處理大型作業? 如果您的程式碼牽涉到大型計算,或需要查詢某些遠端伺服器上的資料庫,又該怎麼做? 通常,答案是在個別執行緒中處理大型作業,讓 UI 執行緒自由地傾向于佇列中的 Dispatcher 專案。 當大型作業完成時,它可以將其結果回報給 UI 執行緒以供顯示。

從歷史上看,Windows 只允許由建立它們的執行緒存取 UI 元素。 這表示負責某些長時間執行工作的背景執行緒無法在完成時更新文字方塊。 Windows 會執行此作業,以確保 UI 元件的完整性。 如果背景執行緒已在繪製期間更新了清單方塊的內容,則該清單方塊看起來可能很奇怪。

WPF 具有內建的相互排除機制,可強制執行此協調。 WPF 中的大部分類別都是衍生自 DispatcherObject 。 在建構時,會 DispatcherObject 儲存連結至目前執行中線程之 的參考 Dispatcher 。 實際上,會 DispatcherObject 與建立它的執行緒產生關聯。 在程式執行期間, DispatcherObject 可以呼叫其公用 VerifyAccess 方法。 VerifyAccessDispatcher 檢查與目前線程相關聯的 ,並將它與建構期間儲存的 Dispatcher 參考進行比較。 如果它們不相符, VerifyAccess 則會擲回例外狀況。 VerifyAccess 是要在屬於 DispatcherObject 的每個方法開頭呼叫 。

如果只有一個執行緒可以修改 UI,背景執行緒如何與使用者互動? 背景執行緒可以要求 UI 執行緒代表其執行作業。 其方式是向 Dispatcher UI 執行緒的 註冊工作專案。 類別 Dispatcher 提供註冊工作專案的方法: Dispatcher.InvokeAsyncDispatcher.BeginInvokeDispatcher.Invoke 。 這些方法會排程委派來執行。 Invoke 是同步呼叫 – 也就是說,在 UI 執行緒實際完成執行委派之前,不會傳回 。 InvokeAsyncBeginInvoke 是非同步,而且會立即傳回。

Dispatcher 依優先順序排序其佇列中的專案。 將元素新增至 Dispatcher 佇列時,可能會指定十個層級。 列舉中 DispatcherPriority 會維護這些優先順序。

具有長時間執行計算的單一執行緒應用程式

大部分的圖形化使用者介面 (GUIS) 會在等候回應使用者互動時,花費大部分的時間閒置。 透過謹慎的程式設計,這個閒置時間可以建設性地使用,而不會影響 UI 的回應性。 WPF 執行緒模型不允許輸入中斷 UI 執行緒中發生的作業。 這表示您必須定期返回 Dispatcher ,以在輸入事件過時之前,定期處理擱置的輸入事件。

示範本節概念的範例應用程式可以從 GitHub 下載 C # Visual Basic

請考慮下列範例:

Screenshot that shows threading of prime numbers.

這個簡單的應用程式會從三開始向上計算,以搜尋質數。 當使用者按一下 [Start (開始)] 按鈕時,開始搜尋。 當程式找到質數時,會使用它的發現來更新使用者介面。 使用者隨時都能停止搜尋。

雖然夠簡單,但質數搜尋會永無止盡的繼續執行,其中會遇到一些難題。 如果我們在按鈕的 Click 事件處理常式內處理整個搜尋,我們絕不會讓 UI 執行緒有機會處理其他事件。 UI 無法回應輸入或處理訊息。 它永遠不會重新繪製,而且永遠不會回應按鈕 Click。

我們可以在個別執行緒中管理質數搜尋,但接著需要處理同步問題。 使用單一執行緒的方法,我們可以直接更新標籤,以列出所找到的最大質數。

如果我們將計算工作分成可管理的區塊,我們可以定期返回 Dispatcher 和 處理事件。 我們可以讓 WPF 有機會重新繪出和處理輸入。

在計算和事件處理之間分割處理時間的最佳方式,就是從 Dispatcher 管理計算。 藉由使用 InvokeAsync 方法,我們可以在 UI 事件從中繪製的相同佇列中排程質數檢查。 在範例中,我們一次只會排程單一質數檢查。 質數檢查完成之後,我們會立即排程下次檢查。 只有在處理暫止的 UI 事件之後,才會進行這項檢查。

Screenshot that shows the dispatcher queue.

Microsoft Word 會使用此機制完成拼字檢查。 使用 UI 執行緒的閒置時間在背景中完成拼字檢查。 讓我們看看程式碼。

下列範例顯示建立使用者介面的 XAML。

重要

本文中顯示的 XAML 來自 C# 專案。 在宣告 XAML 的備份類別時,Visual Basic XAML 稍有不同。

<Window x:Class="SDKSamples.PrimeNumber"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Prime Numbers" Width="360" Height="100">
    <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
        <Button Content="Start"  
                Click="StartStopButton_Click"
                Name="StartStopButton"
                Margin="5,0,5,0" Padding="10,0" />
        
        <TextBlock Margin="10,0,0,0">Biggest Prime Found:</TextBlock>
        <TextBlock Name="bigPrime" Margin="4,0,0,0">3</TextBlock>
    </StackPanel>
</Window>

下列範例顯示程式碼後置。

using System;
using System.Windows;
using System.Windows.Threading;

namespace SDKSamples
{
    public partial class PrimeNumber : Window
    {
        // Current number to check
        private long _num = 3;
        private bool _runCalculation = false;

        public PrimeNumber() =>
            InitializeComponent();

        private void StartStopButton_Click(object sender, RoutedEventArgs e)
        {
            _runCalculation = !_runCalculation;

            if (_runCalculation)
            {
                StartStopButton.Content = "Stop";
                StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
            }
            else
                StartStopButton.Content = "Resume";
        }

        public void CheckNextNumber()
        {
            // Reset flag.
            _isPrime = true;

            for (long i = 3; i <= Math.Sqrt(_num); i++)
            {
                if (_num % i == 0)
                {
                    // Set not a prime flag to true.
                    _isPrime = false;
                    break;
                }
            }

            // If a prime number, update the UI text
            if (_isPrime)
                bigPrime.Text = _num.ToString();

            _num += 2;
            
            // Requeue this method on the dispatcher
            if (_runCalculation)
                StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
        }

        private bool _isPrime = false;
    }
}
Imports System.Windows.Threading

Public Class PrimeNumber
    ' Current number to check
    Private _num As Long = 3
    Private _runCalculation As Boolean = False

    Private Sub StartStopButton_Click(sender As Object, e As RoutedEventArgs)
        _runCalculation = Not _runCalculation

        If _runCalculation Then
            StartStopButton.Content = "Stop"
            StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
        Else
            StartStopButton.Content = "Resume"
        End If

    End Sub

    Public Sub CheckNextNumber()
        ' Reset flag.
        _isPrime = True

        For i As Long = 3 To Math.Sqrt(_num)
            If (_num Mod i = 0) Then

                ' Set Not a prime flag to true.
                _isPrime = False
                Exit For
            End If
        Next

        ' If a prime number, update the UI text
        If _isPrime Then
            bigPrime.Text = _num.ToString()
        End If

        _num += 2

        ' Requeue this method on the dispatcher
        If (_runCalculation) Then
            StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
        End If
    End Sub

    Private _isPrime As Boolean
End Class

除了更新 上的 Button 文字之外, StartStopButton_Click 處理常式還負責藉由將委派新增至 Dispatcher 佇列來排程第一個質數檢查。 此事件處理常式完成其工作之後,會 Dispatcher 選取要執行的委派。

如先前所述, InvokeAsyncDispatcher 用來排程委派執行的成員。 在此情況下,我們會選擇 SystemIdle 優先順序。 只有在沒有要處理的重要事件時,才會 Dispatcher 執行此委派。 UI 回應性比數位檢查更重要。 我們也會傳遞新的委派來代表數字檢查常式。

public void CheckNextNumber()
{
    // Reset flag.
    _isPrime = true;

    for (long i = 3; i <= Math.Sqrt(_num); i++)
    {
        if (_num % i == 0)
        {
            // Set not a prime flag to true.
            _isPrime = false;
            break;
        }
    }

    // If a prime number, update the UI text
    if (_isPrime)
        bigPrime.Text = _num.ToString();

    _num += 2;
    
    // Requeue this method on the dispatcher
    if (_runCalculation)
        StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
}

private bool _isPrime = false;
Public Sub CheckNextNumber()
    ' Reset flag.
    _isPrime = True

    For i As Long = 3 To Math.Sqrt(_num)
        If (_num Mod i = 0) Then

            ' Set Not a prime flag to true.
            _isPrime = False
            Exit For
        End If
    Next

    ' If a prime number, update the UI text
    If _isPrime Then
        bigPrime.Text = _num.ToString()
    End If

    _num += 2

    ' Requeue this method on the dispatcher
    If (_runCalculation) Then
        StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
    End If
End Sub

Private _isPrime As Boolean

這個方法會檢查下一個奇數是否為質數。 如果是質數,方法會直接更新 bigPrimeTextBlock 以反映其探索。 我們可以這麼做,因為計算發生在用來建立控制項的相同執行緒中。 如果我們選擇使用個別執行緒進行計算,我們必須使用更複雜的同步處理機制,並在 UI 執行緒中執行更新。 接下來我們將示範這種情況。

多個視窗、多個執行緒

某些 WPF 應用程式需要多個最上層視窗。 一個執行緒/發送器組合管理多個視窗是完全可以接受的,但有時有數個執行緒可以做得更好。 如果其中一個視窗有可能壟斷執行緒,就特別如此。

Windows 檔案總管會以這種方式運作。 每個新的 [總管] 視窗都屬於原始進程,但它是在獨立執行緒的控制下建立的。 當 Explorer 變成非回應時,例如尋找網路資源時,其他總管視窗會繼續回應且可供使用。

我們可以使用下列範例來示範這個概念。

A screenshot of a WPF window that's duplicated four times. Three of the windows indicate that they're using the same thread, while the other two are on different threads.

此映射的前三個視窗共用相同的執行緒識別碼:1。 另外兩個視窗有不同的執行緒識別碼:Nine 和 4。 每個視窗右上方有一個洋紅色彩旋轉 ! !️ 圖像。

此範例包含具有旋轉 ‼️ 圖像、暫停 按鈕,以及另外兩個 按鈕的視窗,這些按鈕會在目前線程或新執行緒下建立新視窗。 ‼️圖像會持續旋轉,直到 按下 [暫停 ] 按鈕為止,這會暫停執行緒五秒。 視窗底部會顯示執行緒識別碼。

按下 [暫停] 按鈕時,相同執行緒下的所有視窗都會變成非回應。 不同執行緒下的任何視窗會繼續正常運作。

下列範例是視窗的 XAML:

<Window x:Class="SDKSamples.MultiWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Thread Hosted Window" Width="360" Height="180" SizeToContent="Height" ResizeMode="NoResize" Loaded="Window_Loaded">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TextBlock HorizontalAlignment="Right" Margin="30,0" Text="‼️" FontSize="50" FontWeight="ExtraBold"
                   Foreground="Magenta" RenderTransformOrigin="0.5,0.5" Name="RotatedTextBlock">
            <TextBlock.RenderTransform>
                <RotateTransform Angle="0" />
            </TextBlock.RenderTransform>
            <TextBlock.Triggers>
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="RotatedTextBlock"
                                Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
                                From="0" To="360" Duration="0:0:5" RepeatBehavior="Forever" />
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </TextBlock.Triggers>
        </TextBlock>

        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
            <Button Content="Pause" Click="PauseButton_Click" Margin="5,0" Padding="10,0" />
            <TextBlock Margin="5,0,0,0" Text="<-- Pause for 5 seconds" />
        </StackPanel>

        <StackPanel Grid.Row="1" Margin="10">
            <Button Content="Create 'Same Thread' Window" Click="SameThreadWindow_Click" />
            <Button Content="Create 'New Thread' Window" Click="NewThreadWindow_Click" Margin="0,10,0,0" />
        </StackPanel>

        <StatusBar Grid.Row="2" VerticalAlignment="Bottom">
            <StatusBarItem Content="Thread ID" Name="ThreadStatusItem" />
        </StatusBar>

    </Grid>
</Window>

下列範例顯示程式碼後置。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace SDKSamples
{
    public partial class MultiWindow : Window
    {
        public MultiWindow() =>
            InitializeComponent();

        private void Window_Loaded(object sender, RoutedEventArgs e) =>
            ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}";

        private void PauseButton_Click(object sender, RoutedEventArgs e) =>
            Task.Delay(TimeSpan.FromSeconds(5)).Wait();

        private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
            new MultiWindow().Show();

        private void NewThreadWindow_Click(object sender, RoutedEventArgs e)
        {
            Thread newWindowThread = new Thread(ThreadStartingPoint);
            newWindowThread.SetApartmentState(ApartmentState.STA);
            newWindowThread.IsBackground = true;
            newWindowThread.Start();
        }

        private void ThreadStartingPoint()
        {
            new MultiWindow().Show();

            System.Windows.Threading.Dispatcher.Run();
        }
    }
}
Imports System.Threading

Public Class MultiWindow
    Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
        ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}"
    End Sub

    Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs)
        Task.Delay(TimeSpan.FromSeconds(5)).Wait()
    End Sub

    Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim window As New MultiWindow()
        window.Show()
    End Sub

    Private Sub NewThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim newWindowThread = New Thread(AddressOf ThreadStartingPoint)
        newWindowThread.SetApartmentState(ApartmentState.STA)
        newWindowThread.IsBackground = True
        newWindowThread.Start()
    End Sub

    Private Sub ThreadStartingPoint()
        Dim window As New MultiWindow()
        window.Show()

        System.Windows.Threading.Dispatcher.Run()
    End Sub
End Class

以下是要注意的一些詳細資料:

  • 當按下 [暫停 ] 按鈕時 ,工作 Task.Delay(TimeSpan) 會用來讓目前線程暫停五秒。

    private void PauseButton_Click(object sender, RoutedEventArgs e) =>
        Task.Delay(TimeSpan.FromSeconds(5)).Wait();
    
    Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs)
        Task.Delay(TimeSpan.FromSeconds(5)).Wait()
    End Sub
    
  • SameThreadWindow_Click事件處理常式不小心會顯示目前線程下的新視窗。 NewThreadWindow_Click事件處理常式會建立新的執行緒,以開始執行 ThreadStartingPoint 方法,然後顯示新的視窗,如下一個專案符號點所述。

    private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
        new MultiWindow().Show();
    
    private void NewThreadWindow_Click(object sender, RoutedEventArgs e)
    {
        Thread newWindowThread = new Thread(ThreadStartingPoint);
        newWindowThread.SetApartmentState(ApartmentState.STA);
        newWindowThread.IsBackground = true;
        newWindowThread.Start();
    }
    
    Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim window As New MultiWindow()
        window.Show()
    End Sub
    
    Private Sub NewThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim newWindowThread = New Thread(AddressOf ThreadStartingPoint)
        newWindowThread.SetApartmentState(ApartmentState.STA)
        newWindowThread.IsBackground = True
        newWindowThread.Start()
    End Sub
    
  • 方法是 ThreadStartingPoint 新執行緒的起點。 新視窗會在此執行緒的控制下建立。 WPF 會自動建立新的 System.Windows.Threading.Dispatcher 來管理新執行緒。 我們只需要讓視窗正常運作,就是啟動 System.Windows.Threading.Dispatcher

    private void ThreadStartingPoint()
    {
        new MultiWindow().Show();
    
        System.Windows.Threading.Dispatcher.Run();
    }
    
    Private Sub ThreadStartingPoint()
        Dim window As New MultiWindow()
        window.Show()
    
        System.Windows.Threading.Dispatcher.Run()
    End Sub
    

示範本節概念的範例應用程式可以從 GitHub 下載 C # Visual Basic

使用 Task.Run 處理封鎖作業

處理圖形應用程式中的封鎖作業可能很困難。 我們不想從事件處理常式呼叫封鎖方法,因為應用程式似乎凍結。 上述範例會在自己的執行緒中建立新的視窗,讓每個視窗彼此獨立執行。 雖然我們可以使用 建立新的執行緒 System.Windows.Threading.Dispatcher ,但工作完成後,很難將新執行緒與主要 UI 執行緒同步處理。 由於新執行緒無法直接修改 UI,因此我們必須使用 Dispatcher.InvokeAsyncDispatcher.BeginInvokeDispatcher.Invoke ,將委派 Dispatcher 插入 UI 執行緒的 。 最後,這些委派會以修改 UI 元素的許可權來執行。

在同步處理結果時,有一個更簡單的方式,在新的執行緒上執行程式碼,也就是 以工作為基礎的非同步模式(TAP)。 其是以 命名空間中的 System.Threading.TasksTask<TResult> 類型為基礎 Task ,用來表示非同步作業。 TAP 使用單一方法表示非同步作業的啟始和完成。 此模式有幾個優點:

  • Task 呼叫端可以選擇以非同步或同步方式執行程式碼。
  • 您可以從 回報 Task 進度。
  • 呼叫端程式碼可以暫停執行,並等候作業的結果。

Task.Run 範例

在此範例中,我們模仿遠端程序呼叫來擷取氣象預報。 按一下按鈕時,UI 會更新以指出資料擷取正在進行中,而工作會開始模擬擷取天氣預報。 當工作啟動時,按鈕事件處理常式程式碼會暫停,直到工作完成為止。 工作完成之後,事件處理常式程式碼會繼續執行。 程式碼已暫停,而且不會封鎖 UI 執行緒的其餘部分。 WPF 的同步處理內容會暫止程式碼,讓 WPF 繼續執行。

A diagram that demonstrates the workflow of the example app.

示範範例應用程式工作流程的圖表。 應用程式具有單一按鈕,其中包含「擷取預測」文字。 按下按鈕之後,有一個指向應用程式下一個階段的箭號,這是放置在應用程式中央的時鐘影像,指出應用程式正忙於擷取資料。 經過一段時間之後,應用程式會根據資料的結果傳回太陽或雨雲的影像。

示範本節概念的範例應用程式可以從 GitHub 下載 C # Visual Basic 。 此範例的 XAML 相當大,本文並未提供。 使用先前的 GitHub 連結來流覽 XAML。 XAML 會使用單一按鈕來擷取天氣。

請考慮 XAML 的程式碼後置:

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Threading.Tasks;

namespace SDKSamples
{
    public partial class Weather : Window
    {
        public Weather() =>
            InitializeComponent();

        private async void FetchButton_Click(object sender, RoutedEventArgs e)
        {
            // Change the status image and start the rotation animation.
            fetchButton.IsEnabled = false;
            fetchButton.Content = "Contacting Server";
            weatherText.Text = "";
            ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);

            // Asynchronously fetch the weather forecast on a different thread and pause this code.
            string weather = await Task.Run(FetchWeatherFromServerAsync);

            // After async data returns, process it...
            // Set the weather image
            if (weather == "sunny")
                weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];

            else if (weather == "rainy")
                weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];

            //Stop clock animation
            ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
            ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
            
            //Update UI text
            fetchButton.IsEnabled = true;
            fetchButton.Content = "Fetch Forecast";
            weatherText.Text = weather;
        }

        private async Task<string> FetchWeatherFromServerAsync()
        {
            // Simulate the delay from network access
            await Task.Delay(TimeSpan.FromSeconds(4));

            // Tried and true method for weather forecasting - random numbers
            Random rand = new Random();

            if (rand.Next(2) == 0)
                return "rainy";
            
            else
                return "sunny";
        }

        private void HideClockFaceStoryboard_Completed(object sender, EventArgs args) =>
            ((Storyboard)Resources["ShowWeatherImageStoryboard"]).Begin(ClockImage);

        private void HideWeatherImageStoryboard_Completed(object sender, EventArgs args) =>
            ((Storyboard)Resources["ShowClockFaceStoryboard"]).Begin(ClockImage, true);
    }
}
Imports System.Windows.Media.Animation

Public Class Weather

    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)

        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)

        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)

        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)

        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)

        End If

        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)

        ' Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weatherType
    End Sub

    Private Async Function FetchWeatherFromServerAsync() As Task(Of String)

        ' Simulate the delay from network access
        Await Task.Delay(TimeSpan.FromSeconds(4))

        ' Tried and true method for weather forecasting - random numbers
        Dim rand As New Random()

        If rand.Next(2) = 0 Then
            Return "rainy"
        Else
            Return "sunny"
        End If

    End Function

    Private Sub HideClockFaceStoryboard_Completed(sender As Object, e As EventArgs)
        DirectCast(Resources("ShowWeatherImageStoryboard"), Storyboard).Begin(ClockImage)
    End Sub

    Private Sub HideWeatherImageStoryboard_Completed(sender As Object, e As EventArgs)
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Begin(ClockImage, True)
    End Sub
End Class

以下是一些要注意的詳細資料。

  • 按鈕事件處理常式

    private async void FetchButton_Click(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);
    
        // Asynchronously fetch the weather forecast on a different thread and pause this code.
        string weather = await Task.Run(FetchWeatherFromServerAsync);
    
        // After async data returns, process it...
        // Set the weather image
        if (weather == "sunny")
            weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];
    
        else if (weather == "rainy")
            weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];
    
        //Stop clock animation
        ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
        ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
        
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;
    }
    
    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)
    
        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)
    
        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)
    
        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)
    
        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)
    
        End If
    
        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)
    
        ' Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weatherType
    End Sub
    

    請注意,事件處理常式是以 async (或使用 Async Visual Basic 宣告的)。 呼叫等候的方法,例如 FetchWeatherFromServerAsync , 時,「非同步」方法允許暫停程式碼。 這是由 await (或使用 Await Visual Basic) 關鍵字所指定。 FetchWeatherFromServerAsync在 完成之前,按鈕的處理常式代碼會暫停,並將控制項傳回給呼叫端。 這類似于同步方法,不同之處在于同步方法會等候 方法中的每個作業完成,之後再將控制項傳回給呼叫端。

    Awaited 方法會利用目前方法的執行緒內容,而這個方法具有按鈕處理常式,是 UI 執行緒。 這表示呼叫 (Or Await FetchWeatherFromServerAsync() with Visual Basic) 會導致程式碼在 FetchWeatherFromServerAsync UI 執行緒上執行,但在發送器上執行的程式碼有時間執行,類似于具有長時間執行計算 範例的單一線程應用程式運作方式 await FetchWeatherFromServerAsync(); 不過,請注意 await Task.Run ,已使用 。 這會在指定的工作執行緒集區上建立新的執行緒,而不是目前線程。 因此 FetchWeatherFromServerAsync ,會在自己的執行緒上執行。

  • 擷取氣象

    private async Task<string> FetchWeatherFromServerAsync()
    {
        // Simulate the delay from network access
        await Task.Delay(TimeSpan.FromSeconds(4));
    
        // Tried and true method for weather forecasting - random numbers
        Random rand = new Random();
    
        if (rand.Next(2) == 0)
            return "rainy";
        
        else
            return "sunny";
    }
    
    Private Async Function FetchWeatherFromServerAsync() As Task(Of String)
    
        ' Simulate the delay from network access
        Await Task.Delay(TimeSpan.FromSeconds(4))
    
        ' Tried and true method for weather forecasting - random numbers
        Dim rand As New Random()
    
        If rand.Next(2) = 0 Then
            Return "rainy"
        Else
            Return "sunny"
        End If
    
    End Function
    

    為了保持簡單,我們實際上在此範例中沒有任何網路程式碼。 相反地,我們讓新的執行緒進入睡眠狀態 4 秒,藉以模擬網路存取延遲。 此時,原始 UI 執行緒仍在執行中,並在按鈕的事件處理常式暫停到新執行緒完成之前回應 UI 事件。 為了示範這一點,我們已讓動畫繼續執行,而且您可以調整視窗的大小。 如果 UI 執行緒已暫停或延遲,就不會顯示動畫,而且無法與視窗互動。

    Task.Delay完成時,我們已隨機選取天氣預報,天氣狀態會傳回給來電者。

  • 更新 UI

    private async void FetchButton_Click(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);
    
        // Asynchronously fetch the weather forecast on a different thread and pause this code.
        string weather = await Task.Run(FetchWeatherFromServerAsync);
    
        // After async data returns, process it...
        // Set the weather image
        if (weather == "sunny")
            weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];
    
        else if (weather == "rainy")
            weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];
    
        //Stop clock animation
        ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
        ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
        
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;
    }
    
    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)
    
        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)
    
        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)
    
        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)
    
        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)
    
        End If
    
        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)
    
        ' Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weatherType
    End Sub
    

    當工作完成且 UI 執行緒有時間時,按鈕的事件處理常式的呼叫端 Task.Run 會繼續。 方法的其餘部分會停止時鐘動畫,並選擇影像來描述天氣。 它會顯示此影像,並啟用 [擷取預測] 按鈕。

示範本節概念的範例應用程式可以從 GitHub 下載 C # Visual Basic

技術細節和絆腳石點

下列各節說明您可能會遇到多執行緒的一些詳細資料和絆腳點。

巢狀抽水

有時候無法完全鎖定 UI 執行緒。 讓我們考慮 Show 類別的 MessageBox 方法。 Show 直到使用者按一下 [確定] 按鈕,才會傳回 。 不過,它會建立必須有訊息迴圈才能互動的視窗。 雖然我們正在等待使用者按下 [OK (確定)],但原始的應用程式視窗並不會回應使用者輸入。 不過,它會繼續處理繪製訊息。 原始視窗會在涵蓋並顯示時自行重新繪製。

Screenshot that shows a MessageBox with an OK button

有些執行緒必須負責訊息方塊視窗。 WPF 可以只針對訊息方塊視窗建立新的執行緒,但此執行緒將無法在原始視窗中繪製已停用的專案(請記住先前討論相互排除的討論)。 相反地,WPF 會使用巢狀訊息處理系統。 類別 Dispatcher 包含稱為 PushFrame 的特殊方法,它會儲存應用程式的目前執行點,然後開始新的訊息迴圈。 當巢狀訊息迴圈完成時,執行會在原始 PushFrame 呼叫之後繼續執行。

在此情況下,會在呼叫 MessageBox.Show 時維護程式內容, PushFrame 並啟動新的訊息迴圈來重新繪出背景視窗,並處理訊息方塊視窗的輸入。 當使用者按一下 [確定] 並清除快顯視窗時,巢狀迴圈會在呼叫 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>

當滑鼠左鍵按下橢圓形上方時, handler2 就會執行。 完成後 handler2 ,事件會一起傳遞至 Canvas 物件,該物件會用來 handler1 處理它。 只有當未明確將事件物件標示為已處理時 handler2 ,才會發生這種情況。

處理此事件可能需要 handler2 大量時間。 handler2 可能會使用 PushFrame 來開始不會傳回數小時的巢狀訊息迴圈。 如果 handler2 未將此訊息迴圈完成時將事件標示為已處理,即使事件很舊,也會將事件傳遞至樹狀結構。

重新進入和鎖定

Common Language Runtime (CLR) 的鎖定機制並不像人們想像的那樣運作:一個可能會預期執行緒在要求鎖定時完全停止作業。 實際上,執行緒會繼續接收和處理高優先順序的訊息。 這有助於防止發生鎖死,並讓介面進行最低限度的回應,但它也會造成發生輕微 Bug 的可能性。 絕大多數時候,您不需要知道這一點,但在罕見的情況下(通常涉及 Win32 視窗訊息或 COM STA 元件),這值得了解。

大部分的介面不會以執行緒安全性為考慮來建置,因為開發人員會根據一個以上的執行緒永遠不會存取 UI 的假設運作。 在此情況下,該單一執行緒可能會在非預期時間進行環境變更,導致相互排除機制應該解決的不良影響 DispatcherObject 。 請考慮下列虛擬程式碼:

Diagram that shows threading reentrancy.

大部分時間都是正確的事,但在 WPF 中,有時候這類非預期的重新進入確實會造成問題。 因此,在某些關鍵時間,WPF 會呼叫 DisableProcessing ,這會變更該執行緒的鎖定指令,以使用 WPF 重新進入無鎖定,而不是一般的 CLR 鎖定。

那麼,為什麼 CLR 小組會選擇此行為? 它必須使用 STA COM 物件和完成項執行緒來執行。 當垃圾收集物件時,其 Finalize 方法會在專用完成項執行緒上執行,而不是 UI 執行緒。 問題在於,因為在 UI 執行緒上建立的 COM STA 物件只能在 UI 執行緒上處置。 CLR 會執行 對等的 BeginInvoke (在此案例中使用 Win32 的 SendMessage )。 但是,如果 UI 執行緒忙碌中,完成項執行緒會停止,而且無法處置 COM STA 物件,這會造成嚴重的記憶體流失。 因此,CLR 小組發出了艱難的呼籲,使鎖定工作的方式。

WPF 的工作是避免非預期的重新進入,而不重新引入記憶體流失,這就是為什麼我們不會封鎖隨處重新進入的原因。

另請參閱