Model threading
Windows Presentation Foundation (WPF) dirancang untuk menyelamatkan pengembang dari kesulitan utas. Akibatnya, sebagian besar pengembang WPF tidak menulis antarmuka yang menggunakan lebih dari satu utas. Karena program multithreaded rumit dan sulit di-debug, program tersebut harus dihindari ketika ada solusi utas tunggal.
Tidak peduli seberapa baik dirancang, bagaimanapun, tidak ada kerangka kerja UI yang dapat memberikan solusi utas tunggal untuk setiap jenis masalah. WPF mendekati, tetapi masih ada situasi di mana beberapa utas meningkatkan respons antarmuka pengguna (UI) atau performa aplikasi. Setelah membahas beberapa materi latar belakang, artikel ini mengeksplorasi beberapa situasi ini dan kemudian menyimpulkan dengan diskusi tentang beberapa detail tingkat bawah.
Catatan
Topik ini membahas utas dengan menggunakan InvokeAsync metode untuk panggilan asinkron. Metode InvokeAsync
ini mengambil Action atau Func<TResult> sebagai parameter, dan mengembalikan DispatcherOperation atau DispatcherOperation<TResult>, yang memiliki Task properti . Anda dapat menggunakan await
kata kunci dengan DispatcherOperation atau yang terkait Task. Jika Anda perlu menunggu secara sinkron untuk Task yang dikembalikan oleh DispatcherOperation atau DispatcherOperation<TResult>, panggil DispatcherOperationWait metode ekstensi. Task.Wait Panggilan akan mengakibatkan kebuntuan. Untuk informasi selengkapnya tentang menggunakan Task untuk melakukan operasi asinkron, lihat Pemrograman asinkron berbasis tugas.
Untuk melakukan panggilan sinkron, gunakan Invoke metode , yang juga memiliki kelebihan beban yang mengambil delegasi, Action, atau Func<TResult> parameter.
Gambaran umum dan dispatcher
Biasanya, aplikasi WPF dimulai dengan dua utas: satu untuk menangani penyajian dan satu lagi untuk mengelola UI. Utas penyajian secara efektif berjalan tersembunyi di latar belakang sementara utas UI menerima input, menangani peristiwa, melukis layar, dan menjalankan kode aplikasi. Sebagian besar aplikasi menggunakan satu utas UI, meskipun dalam beberapa situasi, yang terbaik adalah menggunakan beberapa. Kita akan membahas ini dengan contoh nanti.
Utas UI mengantrekan item kerja di dalam objek yang Dispatcherdisebut . Memilih Dispatcher item kerja berdasarkan prioritas dan menjalankan masing-masing item ke penyelesaian. Setiap utas UI harus memiliki setidaknya satu Dispatcher, dan masing-masing Dispatcher dapat menjalankan item kerja tepat dalam satu utas.
Trik untuk membangun aplikasi yang responsif dan ramah pengguna adalah dengan memaksimalkan Dispatcher throughput dengan menjaga item kerja tetap kecil. Dengan cara ini item tidak pernah kedaluarsa duduk dalam Dispatcher antrean menunggu pemrosesan. Setiap penundaan yang dapat dirasakan antara input dan respons dapat membuat pengguna frustrasi.
Bagaimana kemudian aplikasi WPF seharusnya menangani operasi besar? Bagaimana jika kode Anda melibatkan perhitungan besar atau perlu mengkueri database di beberapa server jarak jauh? Biasanya, jawabannya adalah menangani operasi besar dalam utas terpisah, membiarkan utas UI bebas untuk cenderung item dalam Dispatcher antrean. Ketika operasi besar selesai, operasi dapat melaporkan hasilnya kembali ke utas UI untuk ditampilkan.
Secara historis, Windows memungkinkan elemen UI untuk diakses hanya oleh utas yang membuatnya. Ini berarti bahwa utas latar belakang yang bertanggung jawab atas beberapa tugas yang berjalan lama tidak dapat memperbarui kotak teks ketika selesai. Windows melakukan ini untuk memastikan integritas komponen UI. Kotak daftar bisa terlihat aneh jika isinya diperbarui oleh utas latar belakang selama pengecatan.
WPF memiliki mekanisme pengecualian timbal balik bawaan yang memberlakukan koordinasi ini. Sebagian besar kelas dalam WPF berasal dari DispatcherObject. Saat konstruksi, menyimpan DispatcherObject referensi ke yang Dispatcher ditautkan ke utas yang sedang berjalan. Akibatnya DispatcherObject , kaitkan dengan utas yang membuatnya. Selama eksekusi program, dapat DispatcherObject memanggil metode publiknya VerifyAccess . VerifyAccess memeriksa yang Dispatcher terkait dengan utas saat ini dan membandingkannya dengan referensi yang Dispatcher disimpan selama konstruksi. Jika mereka tidak cocok, VerifyAccess melemparkan pengecualian. VerifyAccess dimaksudkan untuk dipanggil di awal setiap metode milik DispatcherObject.
Jika hanya satu utas yang dapat memodifikasi UI, bagaimana utas latar belakang berinteraksi dengan pengguna? Utas latar belakang dapat meminta utas UI untuk melakukan operasi atas namanya. Ini dilakukan dengan mendaftarkan item kerja dengan Dispatcher utas UI. Kelas menyediakan Dispatcher metode untuk mendaftarkan item kerja: Dispatcher.InvokeAsync, , Dispatcher.BeginInvokedan Dispatcher.Invoke. Metode ini menjadwalkan delegasi untuk eksekusi. Invoke
adalah panggilan sinkron - yaitu, tidak kembali sampai utas UI benar-benar selesai menjalankan delegasi. InvokeAsync
dan BeginInvoke
tidak sinkron dan segera kembali.
Urutan Dispatcher elemen dalam antreannya berdasarkan prioritas. Ada sepuluh tingkat yang mungkin ditentukan saat menambahkan elemen ke Dispatcher antrean. Prioritas ini dipertahankan dalam DispatcherPriority enumerasi.
Aplikasi berutas tunggal dengan perhitungan jangka panjang
Sebagian besar antarmuka pengguna grafis (GUI) menghabiskan sebagian besar waktu mereka menganggur sambil menunggu peristiwa yang dihasilkan sebagai respons terhadap interaksi pengguna. Dengan pemrograman yang cermat waktu menganggur ini dapat digunakan secara konstruktif, tanpa memengaruhi responsI UI. Model utas WPF tidak memungkinkan input untuk mengganggu operasi yang terjadi di utas UI. Ini berarti Anda harus yakin untuk kembali ke Dispatcher secara berkala untuk memproses peristiwa input yang tertunda sebelum kedaluarsa.
Contoh aplikasi yang menunjukkan konsep bagian ini dapat diunduh dari GitHub untuk C# atau Visual Basic.
Pertimbangkan contoh berikut:
Aplikasi sederhana ini dihitung ke atas dari tiga, mencari angka utama. Saat pengguna mengklik tombol Mulai , pencarian dimulai. Ketika program menemukan prime, program memperbarui antarmuka pengguna dengan penemuannya. Kapan saja, pengguna dapat menghentikan pencarian.
Meskipun cukup sederhana, pencarian angka utama dapat berlangsung selamanya, yang menghadirkan beberapa kesulitan. Jika kami menangani seluruh pencarian di dalam penanganan aktivitas klik tombol, kami tidak akan pernah memberi utas UI kesempatan untuk menangani peristiwa lain. UI tidak akan dapat merespons input atau memproses pesan. Ini tidak akan pernah mengecat ulang dan tidak pernah merespons klik tombol.
Kita dapat melakukan pencarian nomor utama di utas terpisah, tetapi kemudian kita perlu menangani masalah sinkronisasi. Dengan pendekatan satu utas, kita dapat langsung memperbarui label yang mencantumkan prime terbesar yang ditemukan.
Jika kita memecah tugas perhitungan menjadi potongan yang dapat dikelola, kita dapat secara berkala kembali ke Dispatcher peristiwa dan proses. Kita dapat memberikan WPF kesempatan untuk mengecat ulang dan memproses input.
Cara terbaik untuk membagi waktu pemrosesan antara penghitungan dan penanganan peristiwa adalah dengan mengelola perhitungan dari Dispatcher. Dengan menggunakan metode ini InvokeAsync , kita dapat menjadwalkan pemeriksaan nomor utama dalam antrean yang sama dengan tempat peristiwa UI diambil. Dalam contoh kami, kami hanya menjadwalkan satu pemeriksaan angka utama pada satu waktu. Setelah pemeriksaan nomor primer selesai, kami segera menjadwalkan pemeriksaan berikutnya. Pemeriksaan ini berlanjut hanya setelah peristiwa UI yang tertunda telah ditangani.
Microsoft Word menyelesaikan pemeriksaan ejaan menggunakan mekanisme ini. Pemeriksaan ejaan dilakukan di latar belakang menggunakan waktu menganggur utas UI. Mari kita lihat kodenya.
Contoh berikut menunjukkan XAML yang membuat antarmuka pengguna.
Penting
XAML yang ditampilkan dalam artikel ini berasal dari proyek C#. Visual Basic XAML sedikit berbeda saat mendeklarasikan kelas backing untuk 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>
Contoh berikut menunjukkan kode di belakang.
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
Selain memperbarui teks pada Button, StartStopButton_Click
handler bertanggung jawab untuk menjadwalkan pemeriksaan nomor primer pertama dengan menambahkan delegasi ke Dispatcher antrean. Beberapa saat setelah penanganan aktivitas ini menyelesaikan pekerjaannya, Dispatcher akan memilih delegasi untuk dieksekusi.
Seperti yang kami sebutkan sebelumnya, InvokeAsync adalah anggota yang Dispatcher digunakan untuk menjadwalkan delegasi untuk eksekusi. Dalam hal ini, kami memilih SystemIdle prioritas. Dispatcher Akan menjalankan delegasi ini hanya ketika tidak ada peristiwa penting untuk diproses. ResponsI UI lebih penting daripada pemeriksaan angka. Kami juga meneruskan delegasi baru yang mewakili rutinitas pemeriksaan nomor.
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
Metode ini memeriksa apakah angka ganjil berikutnya prima. Jika prima, metode ini secara langsung memperbarui bigPrime
TextBlock untuk mencerminkan penemuannya. Kita dapat melakukan ini karena perhitungan terjadi di utas yang sama yang digunakan untuk membuat kontrol. Jika kami memilih untuk menggunakan utas terpisah untuk perhitungan, kami harus menggunakan mekanisme sinkronisasi yang lebih rumit dan menjalankan pembaruan di utas UI. Kami akan menunjukkan situasi ini selanjutnya.
Beberapa jendela, beberapa utas
Beberapa aplikasi WPF memerlukan beberapa jendela tingkat atas. Ini sangat dapat diterima untuk satu kombinasi Thread/Dispatcher untuk mengelola beberapa jendela, tetapi kadang-kadang beberapa utas melakukan pekerjaan yang lebih baik. Ini terutama berlaku jika ada kemungkinan salah satu jendela akan memonopoli utas.
Windows Explorer bekerja dengan cara ini. Setiap jendela Explorer baru milik proses asli, tetapi dibuat di bawah kendali utas independen. Ketika Explorer menjadi tidak responsif, seperti saat mencari sumber daya jaringan, jendela Explorer lainnya terus responsif dan dapat digunakan.
Kita dapat menunjukkan konsep ini dengan contoh berikut.
Tiga jendela teratas gambar ini memiliki pengidentifikasi utas yang sama: 1. Dua jendela lainnya memiliki pengidentifikasi utas yang berbeda: Sembilan dan 4. Ada magenta berwarna berputar! !️ glyph di kanan atas setiap jendela.
Contoh ini berisi jendela dengan glyph berputar ‼️
, tombol Jeda , dan dua tombol lain yang membuat jendela baru di bawah utas saat ini atau di utas baru. Glyph ‼️
terus berputar hingga tombol Jeda ditekan, yang menjeda utas selama lima detik. Di bagian bawah jendela, pengidentifikasi utas ditampilkan.
Saat tombol Jeda ditekan, semua jendela di bawah utas yang sama menjadi tidak responsif. Jendela apa pun di bawah utas yang berbeda terus berfungsi secara normal.
Contoh berikut adalah XAML ke jendela:
<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>
Contoh berikut menunjukkan kode di belakang.
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
Berikut ini adalah beberapa detail yang akan dicatat:
Tugas Task.Delay(TimeSpan) ini digunakan untuk menyebabkan utas saat ini dijeda selama lima detik saat tombol Jeda ditekan.
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
Penanganan
SameThreadWindow_Click
aktivitas sangat menunjukkan jendela baru di bawah utas saat ini. PenangananNewThreadWindow_Click
aktivitas membuat utas baru yang mulai mengeksekusiThreadStartingPoint
metode, yang pada gilirannya menunjukkan jendela baru, seperti yang dijelaskan di poin poin berikutnya.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
Metode
ThreadStartingPoint
ini adalah titik awal untuk utas baru. Jendela baru dibuat di bawah kontrol utas ini. WPF secara otomatis membuat baru System.Windows.Threading.Dispatcher untuk mengelola utas baru. Yang harus kita lakukan untuk membuat jendela berfungsi adalah memulai 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
Contoh aplikasi yang menunjukkan konsep bagian ini dapat diunduh dari GitHub untuk C# atau Visual Basic.
Menangani operasi pemblokiran dengan Task.Run
Menangani operasi pemblokiran dalam aplikasi grafis bisa sulit. Kami tidak ingin memanggil metode pemblokiran dari penanganan aktivitas karena aplikasi tampaknya membeku. Contoh sebelumnya membuat jendela baru di utas mereka sendiri, memungkinkan setiap jendela berjalan independen satu sama lain. Meskipun kita dapat membuat utas baru dengan System.Windows.Threading.Dispatcher, menjadi sulit untuk menyinkronkan utas baru dengan utas UI utama setelah pekerjaan selesai. Karena utas baru tidak dapat memodifikasi UI secara langsung, kita harus menggunakan Dispatcher.InvokeAsync, , Dispatcher.BeginInvokeatau Dispatcher.Invoke, untuk menyisipkan delegasi ke Dispatcher dalam utas UI. Akhirnya, delegasi ini dijalankan dengan izin untuk memodifikasi elemen UI.
Ada cara yang lebih mudah untuk menjalankan kode pada utas baru sambil menyinkronkan hasilnya, pola asinkron berbasis Tugas (TAP). Ini didasarkan pada Task jenis dan Task<TResult> di System.Threading.Tasks
namespace layanan, yang digunakan untuk mewakili operasi asinkron. TAP menggunakan satu metode untuk mewakili inisiasi dan penyelesaian operasi asinkron. Ada beberapa manfaat untuk pola ini:
- Pemanggil
Task
dapat memilih untuk menjalankan kode secara asinkron atau sinkron. - Kemajuan dapat dilaporkan dari
Task
. - Kode panggilan dapat menangguhkan eksekusi dan menunggu hasil operasi.
Contoh Task.Run
Dalam contoh ini, kami meniru panggilan prosedur jarak jauh yang mengambil prakiraan cuaca. Ketika tombol diklik, UI diperbarui untuk menunjukkan bahwa pengambilan data sedang berlangsung, sementara tugas mulai meniru pengambilan prakiraan cuaca. Ketika tugas dimulai, kode penanganan aktivitas tombol ditangguhkan hingga tugas selesai. Setelah tugas selesai, kode penanganan aktivitas terus berjalan. Kode ditangguhkan, dan tidak memblokir utas UI lainnya. Konteks sinkronisasi WPF menangani penangguhan kode, yang memungkinkan WPF untuk terus berjalan.
Diagram yang menunjukkan alur kerja aplikasi contoh. Aplikasi ini memiliki satu tombol dengan teks "Ambil Prakiraan." Ada panah yang menunjuk ke fase aplikasi berikutnya setelah tombol ditekan, yang merupakan gambar jam yang ditempatkan di tengah aplikasi yang menunjukkan bahwa aplikasi sibuk mengambil data. Setelah beberapa waktu, aplikasi kembali dengan gambar matahari atau awan hujan, tergantung pada hasil data.
Contoh aplikasi yang menunjukkan konsep bagian ini dapat diunduh dari GitHub untuk C# atau Visual Basic. XAML untuk contoh ini cukup besar dan tidak disediakan dalam artikel ini. Gunakan tautan GitHub sebelumnya untuk menelusuri XAML. XAML menggunakan satu tombol untuk mengambil cuaca.
Pertimbangkan kode-balik ke 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
Berikut ini adalah beberapa detail yang akan dicatat.
Penangan aktivitas tombol
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
Perhatikan bahwa penanganan aktivitas dideklarasikan dengan
async
(atauAsync
dengan Visual Basic). Metode "asinkron" memungkinkan penangguhan kode ketika metode yang ditunggu, sepertiFetchWeatherFromServerAsync
, dipanggil. Ini ditunjukawait
oleh kata kunci (atauAwait
dengan Visual Basic).FetchWeatherFromServerAsync
Hingga selesai, kode handler tombol ditangguhkan dan kontrol dikembalikan ke pemanggil. Ini mirip dengan metode sinkron kecuali bahwa metode sinkron menunggu setiap operasi dalam metode selesai setelah kontrol dikembalikan ke pemanggil.Metode yang ditunggu menggunakan konteks utas dari metode saat ini, yang dengan handler tombol, adalah utas UI. Ini berarti bahwa panggilan
await FetchWeatherFromServerAsync();
(AtauAwait FetchWeatherFromServerAsync()
dengan Visual Basic) menyebabkan kode diFetchWeatherFromServerAsync
berjalan pada utas UI, tetapi tidak dijalankan pada dispatcher memiliki waktu untuk menjalankannya, mirip dengan cara aplikasi Utas tunggal dengan contoh perhitungan yang berjalan lama beroperasi. Namun, perhatikan bahwaawait Task.Run
digunakan. Ini membuat utas baru pada kumpulan utas untuk tugas yang ditunjuk alih-alih utas saat ini. JadiFetchWeatherFromServerAsync
berjalan pada utas sendiri.Mengambil Cuaca
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
Untuk menjaga hal-hal sederhana, kita tidak benar-benar memiliki kode jaringan dalam contoh ini. Sebaliknya, kami mensimulasikan penundaan akses jaringan dengan membuat utas baru kami tidur selama empat detik. Dalam waktu ini, utas UI asli masih berjalan dan merespons peristiwa UI saat penanganan aktivitas tombol dijeda hingga utas baru selesai. Untuk menunjukkan ini, kami telah meninggalkan animasi yang berjalan, dan Anda dapat mengubah ukuran jendela. Jika utas UI dijeda atau tertunda, animasi tidak akan ditampilkan dan Anda tidak dapat berinteraksi dengan jendela.
Task.Delay
Setelah selesai, dan kami telah memilih prakiraan cuaca secara acak, status cuaca dikembalikan ke pemanggil.Memperbarui 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
Ketika tugas selesai dan utas UI memiliki waktu, pemanggil
Task.Run
, penanganan aktivitas tombol, dilanjutkan. Metode lainnya menghentikan animasi jam dan memilih gambar untuk menggambarkan cuaca. Ini menampilkan gambar ini dan mengaktifkan tombol "ambil prakiraan".
Contoh aplikasi yang menunjukkan konsep bagian ini dapat diunduh dari GitHub untuk C# atau Visual Basic.
Detail teknis dan titik tersandung
Bagian berikut menjelaskan beberapa detail dan titik tersandung yang mungkin Anda temui dengan multithreading.
Pemompaan berlapis
Terkadang tidak layak untuk sepenuhnya mengunci utas UI. Mari kita pertimbangkan Show metode MessageBox kelas . Show tidak kembali hingga pengguna mengklik tombol OK. Namun, itu membuat jendela yang harus memiliki perulangan pesan agar interaktif. Saat kami menunggu pengguna mengklik OK, jendela aplikasi asli tidak merespons input pengguna. Namun, itu terus memproses pesan cat. Jendela asli menggambar ulang dirinya sendiri ketika ditutupi dan diungkapkan.
Beberapa utas harus bertanggung jawab atas jendela kotak pesan. WPF dapat membuat utas baru hanya untuk jendela kotak pesan, tetapi utas ini tidak akan dapat melukis elemen yang dinonaktifkan di jendela asli (ingat diskusi sebelumnya tentang pengecualian timbal balik). Sebagai gantinya, WPF menggunakan sistem pemrosesan pesan berlapis. Kelas Dispatcher ini mencakup metode khusus yang disebut PushFrame, yang menyimpan titik eksekusi aplikasi saat ini kemudian memulai perulangan pesan baru. Ketika perulangan pesan berlapis selesai, eksekusi dilanjutkan setelah panggilan asli PushFrame .
Dalam hal ini, PushFrame mempertahankan konteks program pada panggilan ke MessageBox.Show, dan memulai perulangan pesan baru untuk mengecat ulang jendela latar belakang dan menangani input ke jendela kotak pesan. Ketika pengguna mengklik OK dan menghapus jendela pop-up, perulangan berlapis keluar dan kontrol dilanjutkan setelah panggilan ke Show.
Peristiwa yang dirutekan kedaluarsa
Sistem peristiwa yang dirutekan di WPF memberi tahu seluruh pohon ketika peristiwa dinaikkan.
<Canvas MouseLeftButtonDown="handler1"
Width="100"
Height="100"
>
<Ellipse Width="50"
Height="50"
Fill="Blue"
Canvas.Left="30"
Canvas.Top="50"
MouseLeftButtonDown="handler2"
/>
</Canvas>
Ketika tombol mouse kiri ditekan di atas elips, handler2
dijalankan. Setelah handler2
selesai, peristiwa diteruskan ke Canvas objek, yang digunakan handler1
untuk memprosesnya. Ini hanya terjadi jika handler2
tidak secara eksplisit menandai objek peristiwa sebagai ditangani.
Ada kemungkinan bahwa handler2
akan membutuhkan banyak waktu untuk memproses peristiwa ini. handler2
mungkin digunakan PushFrame untuk memulai perulangan pesan berlapis yang tidak kembali selama berjam-jam. Jika handler2
tidak menandai peristiwa sebagai ditangani ketika perulangan pesan ini selesai, peristiwa dilewatkan ke pohon meskipun sudah sangat tua.
Masuk kembali dan penguncian
Mekanisme penguncian runtime bahasa umum (CLR) tidak berperilaku persis seperti yang mungkin dibayangkan; seseorang mungkin mengharapkan utas untuk menghentikan operasi sepenuhnya saat meminta kunci. Dalam aktualitas, utas terus menerima dan memproses pesan berprioritas tinggi. Ini membantu mencegah kebuntuan dan membuat antarmuka responsif minimal, tetapi memperkenalkan kemungkinan untuk bug halus. Sebagian besar waktu Anda tidak perlu tahu apa-apa tentang hal ini, tetapi dalam keadaan yang jarang terjadi (biasanya melibatkan pesan jendela Win32 atau komponen COM STA) ini bisa patut diketahui.
Sebagian besar antarmuka tidak dibangun dengan mengingat keamanan utas karena pengembang bekerja dengan asumsi bahwa UI tidak pernah diakses oleh lebih dari satu utas. Dalam hal ini, utas tunggal tersebut dapat membuat perubahan lingkungan pada waktu yang tidak terduga, menyebabkan efek buruk yang DispatcherObject seharusnya diselesaikan oleh mekanisme pengecualian bersama. Pertimbangkan pseudocode berikut:
Sebagian besar waktu itu adalah hal yang tepat, tetapi ada kalanya dalam WPF di mana reentransi yang tidak terduga seperti itu benar-benar dapat menyebabkan masalah. Jadi, pada waktu kunci tertentu, WPF memanggil , yang mengubah instruksi kunci untuk utas DisableProcessingtersebut untuk menggunakan kunci bebas reentransi WPF, alih-alih kunci CLR biasa.
Jadi mengapa tim CLR memilih perilaku ini? Ini ada hubungannya dengan objek COM STA dan utas finalisasi. Ketika objek dikumpulkan sampah, metodenya Finalize
dijalankan pada utas finalizer khusus, bukan utas UI. Dan di sana terletak masalah, karena objek COM STA yang dibuat pada utas UI hanya dapat dibuang pada utas UI. CLR melakukan setara dengan BeginInvoke (dalam hal ini menggunakan Win32 SendMessage
). Tetapi jika utas UI sibuk, utas finalizer terhenti dan objek COM STA tidak dapat dibuang, yang menciptakan kebocoran memori serius. Jadi tim CLR melakukan panggilan sulit untuk membuat kunci berfungsi seperti yang mereka lakukan.
Tugas untuk WPF adalah menghindari reentransi yang tidak terduga tanpa memperkenalkan kembali kebocoran memori, itulah sebabnya kami tidak memblokir reentrancy di mana-mana.
Lihat juga
.NET Desktop feedback