İş 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.
Not
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.DispatcherOperation DispatcherOperation<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:
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.
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 bigPrime
TextBlock . 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.
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şleyicisiNewThreadWindow_Click
, sonraki madde işareti noktasında açıklandığı gibi yeni bir pencere gösteren yöntemini yürütmeyeThreadStartingPoint
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
Task
bildirilebilir. - Ç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.
Ö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
(veyaAsync
Visual Basic ile) bildirildiğine dikkat edin. "async" yöntemi, gibiFetchWeatherFromServerAsync
bir beklenen yöntem çağrıldığında kodun askıya alınmasını sağlar. Bu, (veyaAwait
Visual Basic ile) anahtar sözcüğü tarafındanawait
belirlenir. BitireneFetchWeatherFromServerAsync
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ınawait FetchWeatherFromServerAsync();
içindeki kodunFetchWeatherFromServerAsync
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ığınaawait 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 nedenleFetchWeatherFromServerAsync
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.
İ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:
Ç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 SendMessage
kullanı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.
.NET Desktop feedback