2018年8月21日 星期二

在 WPF 使用 執行緒的同步內容 SynchronizationContext 進行非同步工作處理問題

在 WPF 使用 執行緒的同步內容 SynchronizationContext 進行非同步工作處理問題

SynchronizationContext類別是一個基底類別,它提供執行緒內容的同步處理需求,它可以幫助我們在在不同的同步情況下,進行進行非同步/同步操作正確行為。若想要更深入去了解 SynchronizationContext 這個類別功能,可以參考 不可或缺的 SynchronizationContext 文章。

了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [SynchronizationContext Class] 的使用方式


了解更多關於 [同步處理原始物件概觀] 的使用方式

在這篇文章中,我們將要來說明在 WPF 使用 執行緒的同步內容 SynchronizationContext 進行非同步工作處理問題,也就是我們在 WPF 專案程式碼開發時候,必須要注意一個很重要的需求,任何想要變更 UI 相關物件或其屬性的程式碼,需要在 主執行緒 ( Main Thread ) 或稱 UI執行緒 ( UI Thread ) 下來執行,若沒有遵守這樣的要求,將會造成您的應用程式發生例外異常的問題。這樣的問題將會發生在您的 WPF 專案中有使用到 多執行緒 Multiple Thread 或者 非同步 Asynchronous 處理需求,若沒有特別注意,將會發生不可預期的例外異常問題。
我們來展示當您進行多執行緒或者非同步工作程式設計並且沒有遵循這樣要求,會發生甚麼樣的問題,以及要如何解決此一問題。在這裡您需要知道 WPF 有繼承 SynchronizationContext 類別,實作出 DispatcherSynchronizationContext 類別,當您在非主執行緒或者UI執行緒下執行程式碼,此時,需要在別的執行緒 Thread 下進行更新與變動 UI 相關物件屬性,您可以透過 DispatcherSynchronizationContext 物件幫助您做到同步內容,如此,相關 UI 異動程式碼就會在主執行緒下執行,也就是不會發生不明例外異常。
底下是我們要測試的 WPF XAML 頁面宣告,在這裡,我們宣告了兩個按鈕, [btnRunAsyncWithoutSynchronizationContext] / [btnRunAsyncWithSynchronizationContext] ,分別要進行沒有使用同步內容與有使用同步內容的測試。底下為執行後的畫面。
WPF SynchronizationContext
XAML
<Window x:Class="執行緒的同步處理內容WPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:執行緒的同步處理內容WPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="60"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <TextBlock
            Grid.Row="0" Grid.ColumnSpan="2"
            x:Name="tbkResult"
            Text="執行結果"
            TextWrapping="Wrap"
            HorizontalAlignment="Center" VerticalAlignment="Center"
            FontSize="30"
            />

        <Button
            Grid.Row="1"
            x:Name="btnRunAsyncWithoutSynchronizationContext"
            Content="執行非同步工作,沒有同步內容" Click="btnRunAsyncWithoutSynchronizationContext_Click"/>

        <Button
            Grid.Row="1" Grid.Column="1"
            x:Name="btnRunAsyncWithSynchronizationContext"
            Content="執行非同步工作,有同步內容" Click="btnRunAsyncWithSynchronizationContext_Click"/>
    </Grid>
</Window>
現在,我們來看看當點選這兩個按鈕後,會發生甚麼問題呢?
  • btnRunAsyncWithoutSynchronizationContext
    當點選這個按鈕之後,將會進行執行非同步工作,沒有同步內容,我們可以從程式碼中看出,當這按鈕點選之後,將會執行這個敘述
    await new System.Net.Http.HttpClient().GetStringAsync("http://www.google.com").ConfigureAwait(false)
    這表示將會讀取 Google 網站上的內容,並且傳回該網頁字串內容,不過,在這裡 我們有呼叫 ConfigureAwait(false),這表示當這個非同步工作執行完成之後,並不會回到現在這個執行緒,而是會從執行池 ThreadPool 內取得一個執行緒,繼續執行剩下的程式碼。關於 ConfigureAwait 說明,可以參考 ConfigureAwait 網頁
    請在 tbkResult.Text = foo; 敘述上設定中斷點,並且執行這個專案,接著點選 [執行非同步工作,沒有同步內容] 按鈕,這個時候將會停留在這個中斷點上。請在監看式視窗中,加入觀察 BeforeSynchronizationContext , BeforeThreadID , AfterSynchronizationContext , AfterThreadID ,結果該會類似下面截圖。
    在這裡我們看到在進行非同步工作方法執行之前,當時是執行緒 ID 是 1 (也就是在主執行緒上,我們可以從下面的執行緒視窗看的出來) ,而且當時 SynchronizationContext.Current 屬性中,是有 System.Windows.Threading.DispatcherSynchronizationContext 這個型別的物件;不過,當非同步工作執行完成後,繼續回到這個事件方法內,我們發現到了,這個時候的執行緒ID變成了 7,並且是一個背景工作執行緒,而且這個時候 SynchronizationContext.Current 屬性內已經變成空值。
    WPF Thread
    請打開 [除錯] > [視窗] > [執行緒] 視窗,我們可以看到這個應用程式有用到的執行緒相關資訊,在這個視窗中,我們看到了 主執行緒 Main Thread / UI 執行緒 UI Thread 是編號為 1 的執行緒,而我們停留在中斷點上的程式碼,則是在執行緒為編號 7 的執行緒上執行。
    WPF Thread
    若在此時我們想要修改任何 UI 控制項的屬性,就會發生問題。您將會得到這個錯誤訊息,這也表示了我們的應用程式將會中止執行了。
    System.InvalidOperationException: '呼叫執行緒無法存取此物件,因為此物件屬於另一個執行緒。'
    System.InvalidOperationException
  • btnRunAsyncWithSynchronizationContext
    當點選這個按鈕之後,將會進行執行非同步工作,有同步內容,我們可以從程式碼中看出,當這按鈕點選之後,將會執行這個敘述
    await new System.Net.Http.HttpClient().GetStringAsync("http://www.google.com");
    這表示將會讀取 Google 網站上的內容,並且傳回該網頁字串內容,不過,在這裡 我沒有呼叫 ConfigureAwait(false),這表示當這個非同步工作執行完成之後,會回到現在這個執行緒,也就是主執行緒或者UI執行緒。
    請在 [btnRunAsyncWithSynchronizationContext_Click] 事件方法內的 tbkResult.Text = foo; 敘述上設定中斷點,並且執行這個專案,接著點選 [執行非同步工作,有同步內容] 按鈕,這個時候將會停留在這個中斷點上。請在監看式視窗中,加入觀察 BeforeSynchronizationContext , BeforeThreadID , AfterSynchronizationContext , AfterThreadID ,結果該會類似下面截圖。
    在這裡我們看到在進行非同步工作方法執行前、後,都是位於執行緒 ID 是 1 (也就是在主執行緒上,我們可以從下面的執行緒視窗看的出來) ,=而且當時 SynchronizationContext.Current 屬性中,都是有 System.Windows.Threading.DispatcherSynchronizationContext 這個型別的物件。
    WPF Thread
    請打開 [除錯] > [視窗] > [執行緒] 視窗,我們可以看到這個應用程式有用到的執行緒相關資訊,在這個視窗中,我們看到了 主執行緒 Main Thread / UI 執行緒 UI Thread 是編號為 1 的執行緒,而我們停留在中斷點上的程式碼,則是在執行緒為編號 1 的執行緒上執行。
    WPF Thread
    若在此時我們想要修改任何 UI 控制項的屬性,就不會發生問題。為什麼呢?
    因為這些程式碼都是在主執行緒上執行的呀
    底下是執行後的程式畫面截圖
    SynchronizationContext
C Sharp / C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace 執行緒的同步處理內容WPF
{
    /// <summary>
    /// MainWindow.xaml 的互動邏輯
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void btnRunAsyncWithoutSynchronizationContext_Click(object sender, RoutedEventArgs e)
        {
            SynchronizationContext BeforeSynchronizationContext = SynchronizationContext.Current;
            int BeforeThreadID = Thread.CurrentThread.ManagedThreadId;
            var foo = await new System.Net.Http.HttpClient().GetStringAsync("http://www.google.com").ConfigureAwait(false);
            SynchronizationContext AfterSynchronizationContext = SynchronizationContext.Current;
            int AfterThreadID = Thread.CurrentThread.ManagedThreadId;
            tbkResult.Text = foo;
        }

        private async void btnRunAsyncWithSynchronizationContext_Click(object sender, RoutedEventArgs e)
        {
            SynchronizationContext BeforeSynchronizationContext = SynchronizationContext.Current;
            int BeforeThreadID = Thread.CurrentThread.ManagedThreadId;
            var foo = await new System.Net.Http.HttpClient().GetStringAsync("http://www.google.com");
            SynchronizationContext AfterSynchronizationContext = SynchronizationContext.Current;
            int AfterThreadID = Thread.CurrentThread.ManagedThreadId;
            tbkResult.Text = foo;
        }
    }
}


了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
了解更多關於 [Thread Class] 的使用方式
了解更多關於 [SynchronizationContext Class] 的使用方式


了解更多關於 [同步處理原始物件概觀] 的使用方式



關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程



沒有留言:

張貼留言