Aracılığıyla paylaş


İş parçacığı modeli

Windows Presentation Foundation (WPF), geliştiricileri iş parçacığı oluşturma güçlüklerinden kurtarmak için tasarlanmıştır. Sonuç olarak, çoğu WPF geliştiricisi birden fazla iş parçacığı kullanan bir arabirim yazmaz. Çok iş parçacıklı programlar karmaşık ve hata ayıklaması zor olduğundan, tek iş parçacıklı çözümler mevcut olduğunda bu programlardan kaçınılmalıdır.

Ancak ne kadar iyi bir mimari oluşturulmuş olursa olsun, hiçbir UI çerçevesi her türlü sorun için tek iş parçacıklı bir çözüm sağlayamaz. WPF yakından gelir, ancak yine de birden çok iş parçacığının kullanıcı arabirimi (UI) yanıt hızını veya uygulama performansını geliştirdiği durumlar vardır. Bazı arka plan malzemelerini ele aldıktan sonra, bu makale bu durumların bazılarını inceler ve bazı alt düzey ayrıntıların tartışılmasıyla sonuçlanmıştır.

Dekont

Bu konu, zaman uyumsuz çağrılar için yöntemini kullanarak InvokeAsync iş parçacığı oluşturmayı ele alır. InvokeAsync yöntemi bir Action veya Func<TResult> parametresi alır ve özelliği olan Task bir DispatcherOperation veya DispatcherOperation<TResult>döndürür. veya ilişkili Taskile DispatcherOperation anahtar sözcüğünü kullanabilirsinizawait. veya tarafından döndürülen için Task zaman uyumlu olarak beklemeniz gerekiyorsa uzantı yöntemini çağırınDispatcherOperationWait.DispatcherOperationDispatcherOperation<TResult> Çağrılması Task.Wait kilitlenmeye neden olur. Zaman uyumsuz işlemler gerçekleştirmek için kullanma Task hakkında daha fazla bilgi için bkz . Görev tabanlı zaman uyumsuz programlama.

Zaman uyumlu bir çağrı yapmak için, bir temsilciAction, veya Func<TResult> parametre alan aşırı yüklemeleri de olan yöntemini kullanınInvoke.

Genel bakış ve dağıtıcı

WPF uygulamaları genellikle iki iş parçacığıyla başlar: biri işleme için, diğeri de kullanıcı arabirimini yönetmek için. Ui iş parçacığı girişi alırken, olayları işlerken, ekranı boyarken ve uygulama kodunu çalıştırırken işleme iş parçacığı arka planda etkili bir şekilde gizli olarak çalışır. Bazı durumlarda birkaç kullanıcı arabirimi iş parçacığı kullanmak en iyisi olsa da çoğu uygulama tek bir ui iş parçacığı kullanır. Bunu daha sonra bir örnekle ele alacağız.

UI iş parçacığı, adlı Dispatcherbir nesnenin içindeki iş öğelerini kuyruğa alır. , Dispatcher iş öğelerini öncelik temelinde seçer ve her birini tamamlayarak çalıştırır. Her ui iş parçacığının en az bir Dispatcheröğesi olmalıdır ve her Dispatcher biri iş öğelerini tam olarak bir iş parçacığında yürütebilir.

Esnek ve kullanıcı dostu uygulamalar oluşturmanın püf noktası, iş öğelerini küçük tutarak aktarım hızını en üst düzeye çıkarmaktır Dispatcher . Bu şekilde öğeler hiçbir zaman kuyrukta Dispatcher işlenmeyi beklerken eski kalmaz. Giriş ve yanıt arasındaki algılanabilir gecikmeler kullanıcıyı hayal kırıklığına uğratabilir.

WPF uygulamalarının büyük işlemleri nasıl işlemesi gerekir? Kodunuz büyük bir hesaplama içeriyorsa veya uzak bir sunucudaki veritabanını sorgulaması gerekiyorsa ne olur? Genellikle yanıt, büyük işlemi ayrı bir iş parçacığında işlemektir ve kullanıcı arabirimi iş parçacığını kuyruktaki Dispatcher öğelerle ilgilenmeye serbest bırakır. Büyük işlem tamamlandığında, sonucunu görüntüleme için ui iş parçacığına geri bildirebilir.

Geçmişte Windows, kullanıcı arabirimi öğelerine yalnızca bunları oluşturan iş parçacığı tarafından erişilmesine izin verir. Bu, uzun süre çalışan bazı görevlerden sorumlu bir arka plan iş parçacığının tamamlandığında metin kutusunu güncelleştiremeyeceği anlamına gelir. Windows, ui bileşenlerinin bütünlüğünü sağlamak için bunu yapar. Bir liste kutusu, içeriği boyama sırasında arka plan yazışması tarafından güncelleştirildiyse garip görünebilir.

WPF,bu koordinasyonu zorlayan yerleşik bir karşılıklı dışlama mekanizmasına sahiptir. WPF'deki sınıfların çoğu dosyasından DispatcherObjecttüretilir. İnşaatta, çalışmakta DispatcherObject olan iş parçacığına Dispatcher bağlı bir başvuru depolar. Aslında, onu DispatcherObject oluşturan iş parçacığı ile ilişkilendirir. Program yürütme sırasında, bir DispatcherObject ortak VerifyAccess yöntemini çağırabilir. VerifyAccess geçerli iş parçacığı ile ilişkili inceler Dispatcher ve inşaat Dispatcher sırasında depolanan başvuru ile karşılaştırır. Eşleşmiyorsa, VerifyAccess bir özel durum oluşturur. VerifyAccess , öğesine ait her yöntemin başında çağrılmaya yöneliktir DispatcherObject.

Kullanıcı arabirimini yalnızca bir iş parçacığı değiştirebiliyorsa, arka plan iş parçacıkları kullanıcıyla nasıl etkileşim kurar? Arka plan iş parçacığı, kullanıcı arabirimi iş parçacığından kendi adına bir işlem gerçekleştirmesini isteyebilir. Bunu, ui iş parçacığına Dispatcher bir iş öğesi kaydederek yapar. Dispatcher sınıfı, iş öğelerini kaydetme yöntemlerini sağlar: Dispatcher.InvokeAsync, Dispatcher.BeginInvokeve Dispatcher.Invoke. Bu yöntemler yürütme için bir temsilci zamanlar. Invoke zaman uyumlu bir çağrıdır; yani kullanıcı arabirimi iş parçacığı temsilciyi yürütmeyi tamamlayana kadar geri dönmez. InvokeAsync ve BeginInvoke zaman uyumsuz olup hemen geri döner.

kuyruğundaki Dispatcher öğeleri önceliğe göre sıralar. Kuyruğa öğe Dispatcher eklenirken belirtilebilen on düzey vardır. Bu öncelikler numaralandırmada DispatcherPriority korunur.

Uzun süre çalışan bir hesaplama ile tek iş parçacıklı uygulama

Çoğu grafik kullanıcı arabirimi (GUI), kullanıcı etkileşimlerine yanıt olarak oluşturulan olayları beklerken zamanlarının büyük bir bölümünü boşta geçirir. Dikkatli programlama ile bu boşta kalma süresi, kullanıcı arabiriminin yanıt hızını etkilemeden yapıcı bir şekilde kullanılabilir. WPF iş parçacığı modeli, girişin ui iş parçacığında gerçekleşen bir işlemi kesintiye uğratmasına izin vermez. Bu, beklemedeki giriş olaylarını eskimeden önce işlemek için düzenli aralıklarla öğesine geri döndüğünüzden Dispatcher emin olmanız gerektiği anlamına gelir.

Bu bölümün kavramlarını gösteren örnek bir uygulama, C# veya Visual Basic için GitHub'dan indirilebilir.

Aşağıdaki örneği inceleyin:

Screenshot that shows threading of prime numbers.

Bu basit uygulama üçten yukarı doğru sayar ve asal sayıları arar. Kullanıcı Başlangıç düğmesine tıkladığında arama başlar. Program bir asal bulduğunda, kullanıcı arabirimini bulmasıyla güncelleştirir. Herhangi bir noktada kullanıcı aramayı durdurabilir.

Yeterince basit olsa da, asal sayı araması sonsuza kadar devam edebilir ve bu da bazı zorluklara neden olur. Düğmenin tıklama olay işleyicisinin içindeki aramanın tamamını işleseydik, kullanıcı arabirimi iş parçacığına diğer olayları işleme şansı vermemiş olurduk. Kullanıcı arabirimi, giriş veya işlem iletilerine yanıt veremedi. Hiçbir zaman yeniden boyanmazdı ve düğme tıklamalarına hiçbir zaman yanıt vermez.

Asal sayı aramasını ayrı bir iş parçacığında gerçekleştirebiliriz, ancak ardından eşitleme sorunlarıyla ilgilenmemiz gerekir. Tek iş parçacıklı bir yaklaşımla, bulunan en büyük asal öğesini listeleyen etiketi doğrudan güncelleştirebiliriz.

Hesaplama görevini yönetilebilir öbeklere ayırırsak, ve olaylarına Dispatcher düzenli aralıklarla geri dönebiliriz. WPF'ye girişi yeniden boyama ve işleme fırsatı verebiliriz.

İşlem süresini hesaplama ve olay işleme arasında bölmenin en iyi yolu, hesaplamayı Dispatcheriçinden yönetmektir. yöntemini kullanarak InvokeAsync , kullanıcı arabirimi olaylarının çekildiği kuyrukta asal sayı denetimleri zamanlayabiliriz. Örneğimizde, bir kerede yalnızca tek bir asal sayı denetimi zamanlıyoruz. Asal sayı denetimi tamamlandıktan sonra, bir sonraki denetimi hemen zamanlarız. Bu denetim yalnızca bekleyen kullanıcı arabirimi olayları işlendikten sonra devam eder.

Screenshot that shows the dispatcher queue.

Microsoft Word, bu mekanizmayı kullanarak yazım denetimini gerçekleştirir. Yazım denetimi, kullanıcı arabirimi iş parçacığının boşta kalma süresi kullanılarak arka planda yapılır. Koda bir göz atalım.

Aşağıdaki örnekte kullanıcı arabirimini oluşturan XAML gösterilmektedir.

Önemli

Bu makalede gösterilen XAML bir C# projesinden alınmıştı. Visual Basic XAML, XAML için yedekleme sınıfı bildirilirken biraz farklıdır.

<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>

Aşağıdaki örnekte arka planda kod gösterilmektedir.

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

üzerindeki ButtonStartStopButton_Click metni güncelleştirmenin yanı sıra, işleyici kuyruğa bir temsilci ekleyerek ilk asal sayı denetimini zamanlamaktan Dispatcher sorumludur. Bu olay işleyicisi çalışmasını tamamladıktan bir süre sonra yürütme Dispatcher için temsilciyi seçer.

Daha önce de belirttiğimiz gibi, InvokeAsync yürütme için bir temsilci zamanlamak için kullanılan üyedir Dispatcher . Bu durumda önceliği seçeriz SystemIdle . Dispatcher bu temsilciyi yalnızca işlenecek önemli bir olay olmadığında yürütür. Kullanıcı arabirimi yanıt hızı, sayı denetiminden daha önemlidir. Ayrıca, sayı denetimi yordamını temsil eden yeni bir temsilci geçiririz.

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

Bu yöntem, bir sonraki tek sayinin asal olup olmadığını denetler. Birincil ise, yöntemi bulma işlemini yansıtacak şekilde öğesini doğrudan güncelleştirir bigPrimeTextBlock . Hesaplama, denetimi oluşturmak için kullanılan aynı iş parçacığında gerçekleştiğinden bunu yapabiliriz. Hesaplama için ayrı bir iş parçacığı kullanmayı seçseydik, daha karmaşık bir eşitleme mekanizması kullanmalı ve ui iş parçacığında güncelleştirmeyi yürütmeliyiz. Şimdi bu durumu göstereceğiz.

Birden çok pencere, birden çok iş parçacığı

Bazı WPF uygulamaları birden çok üst düzey pencere gerektirir. Tek bir İş Parçacığı/Dağıtıcı birleşiminin birden çok pencereyi yönetmesi son derece kabul edilebilir, ancak bazen birkaç iş parçacığı daha iyi bir iş yapar. Bu durum özellikle pencerelerden birinin iş parçacığını tekeline alma olasılığı varsa geçerlidir.

Windows Gezgini bu şekilde çalışır. Her yeni Gezgin penceresi özgün işleme aittir, ancak bağımsız bir iş parçacığının denetimi altında oluşturulur. Explorer, ağ kaynaklarını ararken olduğu gibi yanıt vermemeye başladığında, diğer Explorer pencereleri yanıt vermeye ve kullanılabilir olmaya devam eder.

Bu kavramı aşağıdaki örnekle gösterebiliriz.

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.

Bu görüntünün ilk üç penceresi aynı iş parçacığı tanımlayıcısını paylaşır: 1. Diğer iki pencere farklı iş parçacığı tanımlayıcılarına sahiptir: Dokuz ve 4. Her pencerenin sağ üst kısmında dönen bir eflatun renkli ! !️ glif vardır.

Bu örnek, dönen ‼️ bir karakteri, Duraklat düğmesini ve geçerli iş parçacığı altında veya yeni bir iş parçacığında yeni bir pencere oluşturan diğer iki düğmeyi içeren bir pencere içerir. ‼️ Glif, duraklat düğmesine basılana kadar sürekli döner ve bu da iş parçacığını beş saniye duraklatır. Pencerenin en altında, iş parçacığı tanımlayıcısı görüntülenir.

Duraklat düğmesine basıldığında, aynı iş parçacığı altındaki tüm pencereler yanıt vermez hale gelir. Farklı bir iş parçacığı altındaki herhangi bir pencere normal çalışmaya devam eder.

Aşağıdaki örnek, pencereye XAML'dir:

<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>

Aşağıdaki örnekte arka planda kod gösterilmektedir.

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

Dikkat edilmesi gereken bazı ayrıntılar şunlardır:

  • GörevTask.Delay(TimeSpan), Duraklat düğmesine basıldığında geçerli iş parçacığının beş saniye boyunca duraklatmasına neden olmak için kullanılır.

    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
    
  • Olay SameThreadWindow_Click işleyicisi, geçerli iş parçacığının altında yeni bir pencere gösterir. Olay işleyicisi NewThreadWindow_Click , sonraki madde işareti noktasında açıklandığı gibi yeni bir pencere gösteren yöntemini yürütmeye ThreadStartingPoint başlayan yeni bir iş parçacığı oluşturur.

    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 yöntemi, yeni iş parçacığının başlangıç noktasıdır. Yeni pencere bu iş parçacığının denetimi altında oluşturulur. WPF, yeni System.Windows.Threading.Dispatcher iş parçacığını yönetmek için otomatik olarak yeni bir oluşturur. Pencereyi işlevsel hale getirmek için yapmamız gereken tek şey başlatmaktır 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
    

Bu bölümün kavramlarını gösteren örnek bir uygulama, C# veya Visual Basic için GitHub'dan indirilebilir.

Task.Run ile engelleme işlemini işleme

Grafik uygulamadaki engelleme işlemlerini işlemek zor olabilir. Uygulama donuyor gibi göründüğünden, olay işleyicilerinden engelleme yöntemlerini çağırmak istemiyoruz. Önceki örnek kendi iş parçacığında yeni pencereler oluşturarak her pencerenin birbirinden bağımsız çalışmasını sağlar. ile System.Windows.Threading.Dispatcheryeni bir iş parçacığı oluşturabiliriz ancak iş tamamlandıktan sonra yeni iş parçacığını ana kullanıcı arabirimi iş parçacığıyla eşitlemek zorlaşır. Yeni iş parçacığı kullanıcı arabirimini doğrudan değiştiremediğinden, kullanıcı arabirimi iş parçacığına temsilci Dispatcher eklemek için , Dispatcher.BeginInvokeveya Dispatcher.Invokekullanmamız Dispatcher.InvokeAsyncgerekir. Sonunda, bu temsilciler kullanıcı arabirimi öğelerini değiştirme izniyle yürütülür.

Sonuçları, Görev tabanlı zaman uyumsuz deseni (TAP) eşitlerken kodu yeni bir iş parçacığında çalıştırmanın daha kolay bir yolu vardır. Zaman uyumsuz işlemleri temsil etmek için kullanılan ad alanında System.Threading.Tasks ve Task<TResult> türlerini temel alırTask. TAP, bir zaman uyumsuz işlemin başlangıcını ve tamamlanmasını temsil etmek için tek bir yöntem kullanır. Bu desenin birkaç avantajı vardır:

  • çağıran Task , kodu zaman uyumsuz veya zaman uyumlu olarak çalıştırmayı seçebilir.
  • İlerleme durumu' ndan Taskbildirilebilir.
  • Çağıran kod yürütmeyi askıya alabilir ve işlemin sonucunu bekleyebilir.

Task.Run örneği

Bu örnekte, hava durumu tahminini alan bir uzaktan yordam çağrısını taklit ediyoruz. Düğmeye tıklandığında, kullanıcı arabirimi veri getirme işleminin devam ettiğini gösterecek şekilde güncelleştirilir ve bir görev hava durumu tahminini getirmeyi taklit etmeye başlar. Görev başlatıldığında, görev bitene kadar düğme olay işleyici kodu askıya alınır. Görev tamamlandıktan sonra olay işleyicisi kodu çalışmaya devam eder. Kod askıya alınır ve kullanıcı arabirimi iş parçacığının geri kalanını engellemez. WPF'nin eşitleme bağlamı kodun askıya alınmasıyla ilgilenir ve bu da WPF'nin çalışmaya devam etmesine olanak tanır.

A diagram that demonstrates the workflow of the example app.

Örnek uygulamanın iş akışını gösteren diyagram. Uygulamanın "Tahmini Getir" metnini içeren tek bir düğmesi vardır. Düğmeye basıldıktan sonra uygulamanın bir sonraki aşamasına işaret eden bir ok vardır. Bu, uygulamanın ortasına yerleştirilen ve uygulamanın verileri getirmekle meşgul olduğunu gösteren bir saat görüntüsüdür. Bir süre sonra uygulama, verilerin sonucuna bağlı olarak güneş veya yağmur bulutlarının bir görüntüsüyle birlikte geri döner.

Bu bölümün kavramlarını gösteren örnek bir uygulama, C# veya Visual Basic için GitHub'dan indirilebilir. Bu örneğin XAML'i oldukça büyüktür ve bu makalede sağlanmaz. XAML'ye göz atmak için önceki GitHub bağlantılarını kullanın. XAML, hava durumunu getirmek için tek bir düğme kullanır.

XAML'nin arka planındaki kodu göz önünde bulundurun:

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

Aşağıda, dikkat edilmesi gereken bazı ayrıntılar yer alır.

  • Düğme olay işleyicisi

    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
    

    Olay işleyicisinin ile async (veya Async Visual Basic ile) bildirildiğine dikkat edin. "async" yöntemi, gibi FetchWeatherFromServerAsyncbir beklenen yöntem çağrıldığında kodun askıya alınmasını sağlar. Bu, (veya Await Visual Basic ile) anahtar sözcüğü tarafından await belirlenir. Bitirene FetchWeatherFromServerAsync kadar düğmenin işleyici kodu askıya alınır ve denetim çağırana döndürülür. Bu, zaman uyumlu yönteme benzer, ancak zaman uyumlu bir yöntem yöntemindeki her işlemin tamamlanmasını bekler ve bu işlemden sonra denetimin çağırana döndürülmesini bekler.

    Beklenen yöntemler, düğme işleyicisi ile ui iş parçacığı olan geçerli yöntemin iş parçacığı bağlamını kullanır. Bu, (Veya Await FetchWeatherFromServerAsync() Visual Basic ile) çağrısının await FetchWeatherFromServerAsync(); içindeki kodun FetchWeatherFromServerAsync ui iş parçacığında çalışmasına neden olduğu, ancak dağıtıcıda yürütülmeyen kodun, uzun süre çalışan bir hesaplama örneğine sahip Tek iş parçacıklı uygulamanın nasıl çalıştığına benzer şekilde çalıştırılacak zamanı olduğu anlamına gelir. Ancak, bunun kullanıldığına await Task.Run dikkat edin. Bu, geçerli iş parçacığı yerine belirlenen görev için iş parçacığı havuzunda yeni bir iş parçacığı oluşturur. Bu nedenle FetchWeatherFromServerAsync kendi iş parçacığı üzerinde çalışır.

  • Hava Durumunu Getirme

    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
    

    İşleri basit tutmak için bu örnekte ağ kodu yoktur. Bunun yerine, yeni iş parçacığımızı dört saniye uyku moduna alarak ağ erişiminin gecikmesinin simülasyonunu yapıyoruz. Bu süre içinde, düğmenin olay işleyicisi yeni iş parçacığı tamamlanana kadar duraklatılırken özgün ui iş parçacığı çalışmaya ve ui olaylarına yanıt vermeye devam eder. Bunu göstermek için bir animasyonu çalışır durumda bıraktık ve pencereyi yeniden boyutlandırabilirsiniz. Kullanıcı arabirimi iş parçacığı duraklatıldıysa veya geciktirildiyse animasyon gösterilmez ve pencereyle etkileşim kuramazsınız.

    Task.Delay tamamlandığında ve hava durumu tahminimizi rastgele seçtiğimizde, hava durumu arayana döndürülür.

  • Kullanıcı arabirimini güncelleştirme

    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
    

    Görev tamamlandığında ve ui iş parçacığının zamanı olduğunda, düğmesinin olay işleyicisi Task.Runçağıranı sürdürülür. Yöntemin geri kalanı saat animasyonunu durdurur ve hava durumunu tanımlamak için bir görüntü seçer. Bu görüntüyü görüntüler ve "tahmini getir" düğmesini etkinleştirir.

Bu bölümün kavramlarını gösteren örnek bir uygulama, C# veya Visual Basic için GitHub'dan indirilebilir.

Teknik ayrıntılar ve tökezleme noktaları

Aşağıdaki bölümlerde, çok iş parçacığı kullanımıyla karşılaşabileceğiniz bazı ayrıntılar ve tökezleme noktaları açıklanmaktadır.

İç içe pompalama

Bazen kullanıcı arabirimi iş parçacığını tamamen kilitlemek mümkün değildir. Sınıfının yöntemini MessageBox düşünelimShow. Show kullanıcı Tamam düğmesine tıklayana kadar geri dönmez. Ancak etkileşimli olması için ileti döngüsü olması gereken bir pencere oluşturur. Kullanıcının Tamam'a tıklamasını beklerken, özgün uygulama penceresi kullanıcı girişine yanıt vermiyor. Ancak boya iletilerini işlemeye devam eder. Orijinal pencere, kaplandığında ve ortaya çıktığında kendini yeniden çizer.

Screenshot that shows a MessageBox with an OK button

İleti kutusu penceresinden bazı iş parçacıklarının sorumlu olması gerekir. WPF yalnızca ileti kutusu penceresi için yeni bir iş parçacığı oluşturabilir, ancak bu iş parçacığı özgün penceredeki devre dışı bırakılmış öğeleri boyayamaz (karşılıklı dışlamanın önceki tartışmasını anımsayın). Bunun yerine WPF iç içe ileti işleme sistemi kullanır. sınıfı, Dispatcher bir uygulamanın geçerli yürütme noktasını depolayan ve ardından yeni bir ileti döngüsü başlatan adlı PushFrameözel bir yöntem içerir. İç içe ileti döngüsü tamamlandığında, yürütme özgün PushFrame çağrıdan sonra devam eder.

Bu durumda, PushFrame çağrısında MessageBox.Showprogram bağlamını korur ve arka plan penceresini yeniden boyamak ve ileti kutusu penceresine girişi işlemek için yeni bir ileti döngüsü başlatır. Kullanıcı Tamam'a tıkladığında ve açılır pencereyi temizlediğinde, iç içe döngüden çıkar ve denetimi çağrısından Showsonra devam eder.

Eski yönlendirilmiş olaylar

WPF'deki yönlendirilmiş olay sistemi, olaylar tetiklendiğinde tüm ağaçlara bildirir.

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

Sol fare düğmesine üç noktanın üzerinde basıldığında yürütülür handler2 . Tamamlandıktan sonra handler2 , olay nesneye Canvas geçirilir ve bunu işlemek için kullanılır handler1 . Bu yalnızca olay nesnesini açıkça işlenmiş olarak işaretlemezse handler2 gerçekleşir.

Bu handler2 olayın işlenmesi çok zaman alabilir. handler2 , saatler boyunca döndürülmeyen iç içe ileti döngüsü başlatmak için kullanabilir PushFrame . Bu ileti döngüsü tamamlandığında olayı işleniyor olarak işaretlemezse handler2 , olay çok eski olsa bile ağaçtan geçirilir.

Yeniden giriş ve kilitleme

Ortak dil çalışma zamanının (CLR) kilitleme mekanizması tam olarak tahmin edebileceğiniz gibi davranmaz; bir iş parçacığının kilit isteğinde bulunurken işlemi tamamen durdurmasını bekleyebilir. Aslında, iş parçacığı yüksek öncelikli iletileri almaya ve işlemeye devam eder. Bu, kilitlenmeleri önlemeye ve arabirimlerin minimum düzeyde yanıt vermesine yardımcı olur, ancak küçük hatalar için olasılık getirir. Çoğu zaman bu konuda hiçbir şey bilmeniz gerekmez, ancak nadir durumlarda (genellikle Win32 pencere iletilerini veya COM STA bileşenlerini içerir) bu bilmeye değer olabilir.

Geliştiriciler bir kullanıcı arabirimine hiçbir zaman birden fazla iş parçacığı tarafından erişilmediği varsayımıyla çalıştığından çoğu arabirim iş parçacığı güvenliği göz önünde bulundurularak derlenmez. Bu durumda, bu tek iş parçacığı beklenmeyen zamanlarda çevresel değişiklikler yapabilir ve karşılıklı dışlama mekanizmasının çözmesi DispatcherObject gereken bu kötü etkilere neden olabilir. Aşağıdaki sahte kodu göz önünde bulundurun:

Diagram that shows threading reentrancy.

Çoğu zaman bu doğru şeydir, ancak WPF'de böyle beklenmedik yeniden girişlerin gerçekten sorunlara neden olabileceği zamanlar vardır. Bu nedenle, belirli anahtar zamanlarında WPF çağrıları DisableProcessing, bu iş parçacığının kilit yönergesini normal CLR kilidi yerine WPF yeniden girişsiz kilidi kullanacak şekilde değiştirir.

Peki CLR ekibi neden bu davranışı seçti? BUNUN COM STA nesneleri ve sonlandırma iş parçacığıyla ilgisi vardı. Bir nesne çöp olarak toplandığında, Finalize yöntemi ui iş parçacığında değil ayrılmış sonlandırıcı iş parçacığında çalıştırılır. Kullanıcı arabirimi iş parçacığında oluşturulan COM STA nesnesi yalnızca UI iş parçacığında atılabildiği için sorun burada yatıyor. CLR, bir BeginInvoke eşdeğerini yapar (bu örnekte Win32'ler SendMessagekullanılır). Ancak UI iş parçacığı meşgulse sonlandırıcı iş parçacığı durdurulmuş olur ve COM STA nesnesi atılamaz ve bu da ciddi bir bellek sızıntısı oluşturur. Bu nedenle CLR ekibi kilitlerin olduğu gibi çalışması için zor bir çağrı yaptı.

WPF görevi, bellek sızıntısını yeniden başlatmadan beklenmeyen yeniden girişi önlemektir, bu nedenle her yerde yeniden girişi engellemeyiz.

Ayrıca bkz.