Bagikan melalui


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:

Screenshot that shows threading of prime numbers.

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.

Screenshot that shows the dispatcher queue.

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

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.

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. Penanganan NewThreadWindow_Click aktivitas membuat utas baru yang mulai mengeksekusi ThreadStartingPoint 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.

A diagram that demonstrates the workflow of the example app.

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 (atau Async dengan Visual Basic). Metode "asinkron" memungkinkan penangguhan kode ketika metode yang ditunggu, seperti FetchWeatherFromServerAsync, dipanggil. Ini ditunjuk await oleh kata kunci (atau Await 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(); (Atau Await FetchWeatherFromServerAsync() dengan Visual Basic) menyebabkan kode di FetchWeatherFromServerAsync 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 bahwa await Task.Run digunakan. Ini membuat utas baru pada kumpulan utas untuk tugas yang ditunjuk alih-alih utas saat ini. Jadi FetchWeatherFromServerAsync 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.

Screenshot that shows a MessageBox with an OK button

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:

Diagram that shows threading reentrancy.

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.

Baca juga