Konkurensi dan asinkron tingkat lanjut dengan C++/WinRT

Topik ini menjelaskan skenario lanjutan dengan konkurensi dan asinkron di C++/WinRT.

Untuk pengantar tentang topik ini, pertama-tama baca Konkurensi dan operasi asinkron.

Mengalihkan pekerjaan ke thread pool Windows

Koroutine adalah fungsi seperti fungsi lainnya, dalam arti pemanggil akan diblokir hingga fungsi tersebut mengembalikan kontrol eksekusi kepadanya. Dan peluang pertama bagi coroutine untuk kembali adalah co_await, co_return, atau co_yield yang pertama.

Jadi, sebelum Anda melakukan pekerjaan terikat komputasi dalam koroutine, Anda perlu mengembalikan eksekusi ke pemanggil (dengan kata lain, memperkenalkan titik penangguhan) sehingga pemanggil tidak diblokir. Jika Anda belum melakukannya dengan co_await-ing beberapa operasi lain, maka Anda dapat co_await fungsi winrt::resume_background . Itu mengembalikan kontrol ke pemanggil, dan kemudian segera melanjutkan eksekusi pada utas kumpulan utas.

Kumpulan utas yang digunakan dalam implementasi adalah kumpulan utas Windows tingkat rendah, sehingga efisien secara optimal.

IAsyncOperation<uint32_t> DoWorkOnThreadPoolAsync()
{
    co_await winrt::resume_background(); // Return control; resume on thread pool.

    uint32_t result;
    for (uint32_t y = 0; y < height; ++y)
    for (uint32_t x = 0; x < width; ++x)
    {
        // Do compute-bound work here.
    }
    co_return result;
}

Pemrograman dengan mempertimbangkan afinitas utas

Skenario ini diperluas pada yang sebelumnya. Anda mengalihkan sebagian pekerjaan ke thread pool, tetapi kemudian ingin menampilkan kemajuan di antarmuka pengguna (UI).

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    textblock.Text(L"Done!"); // Error: TextBlock has thread affinity.
}

Kode di atas menghasilkan pengecualian winrt::hresult_wrong_thread, karena TextBlock harus diperbarui oleh utas yang membuatnya, yaitu utas UI. Salah satu solusinya adalah menangkap konteks utas tempat coroutine kita awalnya dipanggil. Untuk melakukannya, buat instans objek winrt::apartment_context , lakukan pekerjaan latar belakang, lalu co_awaitapartment_context untuk beralih kembali ke konteks panggilan.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    winrt::apartment_context ui_thread; // Capture calling context.

    co_await winrt::resume_background();
    // Do compute-bound work here.

    co_await ui_thread; // Switch back to calling context.

    textblock.Text(L"Done!"); // Ok if we really were called from the UI thread.
}

Asalkan korutin di atas dipanggil dari utas UI yang membuat TextBlock, teknik ini berfungsi. Akan ada banyak situasi dalam aplikasi Anda saat Anda yakin akan hal tersebut.

Untuk solusi yang lebih umum untuk memperbarui antarmuka pengguna, yang mencakup kasus ketika Anda tidak yakin tentang utas pemanggil, Anda dapat co_await menggunakan fungsi winrt::resume_foreground untuk beralih ke utas latar depan tertentu. Dalam contoh kode di bawah ini, kami menentukan utas latar depan dengan meneruskan antrean dispatcher yang terkait dengan TextBlock (dengan mengakses properti DispatcherQueue-nya). Implementasi winrt::resume_foreground memanggil DispatcherQueue.TryEnqueue pada objek antrean dispatcher tersebut untuk menjalankan pekerjaan yang datang setelahnya dalam coroutine.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    // Switch to the foreground thread associated with textblock.
    co_await winrt::resume_foreground(textblock.DispatcherQueue());

    textblock.Text(L"Done!"); // Guaranteed to work.
}

Fungsi winrt::resume_foreground mengambil parameter prioritas opsional. Jika Anda menggunakan parameter tersebut, maka pola yang ditunjukkan di atas sesuai. Jika tidak, maka Anda dapat memilih untuk menyederhanakan co_await winrt::resume_foreground(someDispatcherObject); menjadi hanya co_await someDispatcherObject;.

Konteks eksekusi, memulai kembali, dan beralih dalam koroutine

Secara umum, setelah titik penangguhan dalam coroutine, utas eksekusi asli mungkin tidak lagi digunakan, dan eksekusi dapat dilanjutkan pada utas mana pun (dengan kata lain, utas mana pun dapat memanggil metode Completed untuk operasi asinkron).

Tetapi jika Anda co_await salah satu dari empat jenis operasi asinkron Windows Runtime (IAsyncXxx), maka C++/WinRT mengambil konteks panggilan pada saat Anda co_await. Ini memastikan bahwa Anda tetap berada dalam konteks tersebut saat kelanjutan dilanjutkan kembali. C++/WinRT melakukan ini dengan memeriksa apakah Anda sudah berada dalam konteks panggilan dan, jika tidak, beralih ke konteks tersebut. Jika Anda berada di utas apartemen berulir tunggal (STA) sebelum co_await, maka Anda akan berada di yang sama setelahnya; jika Anda berada di utas apartemen multi-utas (MTA) sebelum co_await, maka Anda akan berada di satu setelahnya.

IAsyncAction ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;

    // The thread context at this point is captured...
    SyndicationFeed syndicationFeed{ co_await syndicationClient.RetrieveFeedAsync(rssFeedUri) };
    // ...and is restored at this point.
}

Alasan Anda dapat mengandalkan perilaku ini adalah bahwa C++/WinRT menyediakan kode untuk mengadaptasi tipe operasi asinkron Windows Runtime tersebut agar sesuai dengan dukungan coroutine dalam bahasa C++ (potongan-potongan kode ini disebut adapter tunggu). Tipe awaitable yang tersisa di C++/WinRT pada dasarnya hanyalah pembungkus thread pool dan/atau fungsi bantu; jadi penyelesaiannya terjadi di thread pool.

using namespace std::chrono_literals;
IAsyncOperation<int> return_123_after_5s()
{
    // No matter what the thread context is at this point...
    co_await 5s;
    // ...we're on the thread pool at this point.
    co_return 123;
}

Jika Anda co_await tipe lain—bahkan dalam implementasi korutin C++/WinRT—berarti pustaka lain yang menyediakan adaptor, dan Anda perlu memahami cara kerja adaptor tersebut dalam hal pelanjutan eksekusi dan konteks.

Untuk meminimalkan perpindahan konteks, Anda dapat menggunakan beberapa teknik yang telah kita lihat di topik ini. Mari kita lihat beberapa ilustrasi tentang cara melakukannya. Dalam contoh pseudokode berikut, kami menunjukkan kerangka penangan peristiwa yang memanggil API Windows Runtime untuk memuat gambar, beralih ke utas latar belakang untuk memproses gambar tersebut, lalu kembali ke utas UI untuk menampilkan gambar di UI.

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    // Call StorageFile::OpenAsync to load an image file.

    // The call to OpenAsync occurred on a background thread, but C++/WinRT has restored us to the UI thread by this point.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Process the image.

    co_await winrt::resume_foreground(this->DispatcherQueue());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

Untuk skenario ini, terdapat sedikit inefisiensi terkait pemanggilan StorageFile::OpenAsync. Ada pengalihan konteks yang diperlukan ke utas latar belakang (agar handler dapat mengembalikan eksekusi kepada pemanggil), dan saat eksekusi dilanjutkan kembali setelah itu, C++/WinRT memulihkan konteks utas UI. Tapi, dalam kasus ini, tidak perlu berada di thread UI hingga kita hendak memperbarui UI. Semakin banyak API Windows Runtime yang kami panggil sebelum memanggil winrt::resume_background, semakin banyak peralihan konteks bolak-balik yang tidak perlu yang harus kami tanggung. Solusinya adalah tidak memanggil API Windows Runtime apa pun sebelum itu. Pindahkan semuanya ke setelah winrt::resume_background.

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Call StorageFile::OpenAsync to load an image file.

    // Process the image.

    co_await winrt::resume_foreground(this->DispatcherQueue());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

Jika Anda ingin mengerjakan sesuatu yang lebih lanjut, Anda dapat menulis adaptor await Anda sendiri. Misalnya, jika Anda ingin co_await dilanjutkan pada utas yang sama saat operasi asinkron selesai (jadi, tidak ada peralihan konteks), Anda dapat memulai dengan menulis adaptor await yang mirip dengan yang ditunjukkan di bawah ini.

Note

Contoh kode di bawah ini disediakan hanya untuk tujuan pendidikan; itu untuk membuat Anda mulai memahami cara kerja adaptor tunggu. Jika Anda ingin menggunakan teknik ini dalam basis kode Anda sendiri, kami sarankan agar Anda mengembangkan dan menguji struct adaptor await Anda sendiri. Misalnya, Anda dapat menulis complete_on_any, complete_on_current, dan complete_on(dispatcher). Pertimbangkan juga untuk menjadikannya templat yang mengambil jenis IAsyncXxx sebagai parameter templat.

struct no_switch
{
    no_switch(Windows::Foundation::IAsyncAction const& async) : m_async(async)
    {
    }

    bool await_ready() const
    {
        return m_async.Status() == Windows::Foundation::AsyncStatus::Completed;
    }

    void await_suspend(std::experimental::coroutine_handle<> handle) const
    {
        m_async.Completed([handle](Windows::Foundation::IAsyncAction const& /* asyncInfo */, Windows::Foundation::AsyncStatus const& /* asyncStatus */)
        {
            handle();
        });
    }

    auto await_resume() const
    {
        return m_async.GetResults();
    }

private:
    Windows::Foundation::IAsyncAction const& m_async;
};

Untuk memahami cara menggunakan adaptor await no_switch, Anda harus terlebih dahulu mengetahui bahwa ketika pengompilasi C++ menemukan ekspresi co_await, pengompilasi akan mencari fungsi yang disebut await_ready, await_suspend, dan await_resume. Pustaka C++/WinRT menyediakan fungsi-fungsi tersebut sehingga Anda mendapatkan perilaku yang wajar secara default, seperti ini.

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

Untuk menggunakan adaptor await no_switch, cukup ubah tipe ekspresi co_await tersebut dari IAsyncXxx menjadi no_switch, seperti ini.

IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);

Kemudian, alih-alih mencari tiga fungsi await_xxx yang cocok dengan IAsyncXxx, pengkompilasi C++ mencari fungsi yang cocok dengan no_switch.

Penyelaman yang lebih dalam ke winrt::resume_foreground

Pada C++/WinRT 2.0, fungsi winrt::resume_foreground menangguhkan bahkan jika dipanggil dari utas dispatcher (pada versi sebelumnya, fungsi ini dapat menyebabkan kebuntuan dalam beberapa skenario karena hanya ditangguhkan jika belum ada di utas dispatcher).

Perilaku saat ini berarti bahwa Anda dapat mengandalkan stack unwinding dan proses antre ulang berlangsung, dan hal itu penting untuk stabilitas sistem, terutama dalam kode sistem tingkat rendah. Daftar kode terakhir di bagian Pemrograman dengan mempertimbangkan afinitas utas, di atas, mengilustrasikan melakukan beberapa perhitungan kompleks pada utas latar belakang, lalu beralih ke utas UI yang sesuai untuk memperbarui antarmuka pengguna (UI).

Berikut adalah bagaimana winrt::resume_foreground terlihat secara internal.

auto resume_foreground(...) noexcept
{
    struct awaitable
    {
        bool await_ready() const
        {
            return false; // Queue without waiting.
            // return m_dispatcher.HasThreadAccess(); // The C++/WinRT 1.0 implementation.
        }
        void await_resume() const {}
        void await_suspend(coroutine_handle<> handle) const { ... }
    };
    return awaitable{ ... };
};

Perilaku saat ini, versus sebelumnya, dianalogikan dengan perbedaan antara PostMessage dan SendMessage dalam pengembangan aplikasi Win32. PostMessage mengantrekan pekerjaan dan kemudian melepas tumpukan tanpa menunggu pekerjaan selesai. Penguraian tumpukan dapat menjadi sangat penting.

Fungsi winrt::resume_foreground awalnya mendukung CoreDispatcher (terkait dengan CoreWindow), yang diperkenalkan sebelum Windows 10. Di Aplikasi WinUI 3 dan SDK Aplikasi Windows, gunakan DispatcherQueue sebagai gantinya. Anda dapat membuat DispatcherQueue untuk tujuan Anda sendiri. Pertimbangkan aplikasi konsol sederhana ini.

using namespace Windows::System;

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    auto controller{ DispatcherQueueController::CreateOnDedicatedThread() };
    RunAsync(controller.DispatcherQueue());
    getchar();
}

Contoh di atas membuat antrean (yang berada di dalam pengontrol) pada utas privat, kemudian meneruskan pengontrol tersebut ke korutin. Korutin dapat menggunakan antrean untuk menunggu (menangguhkan dan melanjutkan) di utas privat. Penggunaan umum lain dari DispatcherQueue adalah membuat antrean pada utas UI saat ini untuk desktop tradisional atau aplikasi Win32.

DispatcherQueueController CreateDispatcherQueueController()
{
    DispatcherQueueOptions options
    {
        sizeof(DispatcherQueueOptions),
        DQTYPE_THREAD_CURRENT,
        DQTAT_COM_STA
    };
 
    ABI::Windows::System::IDispatcherQueueController* ptr{};
    winrt::check_hresult(CreateDispatcherQueueController(options, &ptr));
    return { ptr, take_ownership_from_abi };
}

Ini menggambarkan bagaimana Anda dapat memanggil dan menggabungkan fungsi Win32 ke dalam proyek C++/WinRT Anda, hanya dengan memanggil fungsi CreateDispatcherQueueController bergaya Win32 untuk membuat pengontrol, lalu mentransfer kepemilikan pengontrol antrean yang dihasilkan ke pemanggil sebagai objek WinRT. Beginilah tepatnya cara Anda dapat mendukung mekanisme antrean yang efisien dan lancar di aplikasi desktop Win32 bergaya Petzold yang sudah ada.

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    Window window;
    auto controller{ CreateDispatcherQueueController() };
    RunAsync(controller.DispatcherQueue());
    MSG message;
 
    while (GetMessage(&message, nullptr, 0, 0))
    {
        DispatchMessage(&message);
    }
}

Di atas, fungsi utama sederhana dimulai dengan membuat jendela. Anda dapat membayangkan bahwa ini mendaftarkan kelas jendela, dan memanggil CreateWindow untuk membuat jendela desktop tingkat atas. Fungsi CreateDispatcherQueueController kemudian dipanggil untuk membuat pengontrol antrean sebelum memanggil beberapa coroutine dengan antrean dispatcher yang dimiliki oleh pengontrol ini. Pompa pesan tradisional kemudian dimasukkan di mana dimulainya kembali koroutine secara alami terjadi pada utas ini. Setelah melakukannya, Anda dapat kembali ke dunia coroutine yang elegan untuk alur kerja berbasis asinkron atau pesan di dalam aplikasi Anda.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ... // Begin on the calling thread...
 
    co_await winrt::resume_foreground(queue);
 
    ... // ...resume on the dispatcher thread.
}

Panggilan ke winrt::resume_foreground akan selalu mengantre, lalu melepas tumpukan. Anda juga dapat mengatur prioritas pelanjutan secara opsional.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
 
    ...
}

Atau, menggunakan urutan antrean default.

...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await queue;
 
    ...
}

Note

Seperti yang ditunjukkan di atas, pastikan untuk menyertakan header proyeksi untuk namespace jenis yang Anda co_await-ing. Misalnya, Windows::System::DispatcherQueue atau Microsoft::UI::Dispatching::DispatcherQueue.

Atau, dalam hal ini, mendeteksi penghentian antrean dan menanganinya dengan baik.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    if (co_await queue)
    {
        ... // Resume on dispatcher thread.
    }
    else
    {
        ... // Still on calling thread.
    }
}

Ekspresi co_await mengembalikan true, yang menunjukkan bahwa pelanjutan eksekusi akan terjadi pada utas dispatcher. Dengan kata lain, antrean itu berhasil. Sebaliknya, sistem akan mengembalikan false untuk menunjukkan bahwa eksekusi tetap berada pada utas pemanggil karena pengontrol antrean sedang dimatikan dan tidak lagi melayani permintaan pada antrean.

Jadi, Anda memiliki kemampuan yang sangat besar saat menggabungkan C++/WinRT dengan korutin, terutama ketika mengembangkan aplikasi desktop bergaya lama ala Petzold.

Membatalkan operasi asinkron, dan panggilan balik pembatalan

Fitur Windows Runtime untuk pemrograman asinkron memungkinkan Anda membatalkan tindakan atau operasi asinkron dalam penerbangan. Berikut adalah contoh yang memanggil StorageFolder::GetFilesAsync untuk mengambil kumpulan file yang berpotensi besar, dan menyimpan objek operasi asinkron yang dihasilkan dalam anggota data. Pengguna memiliki opsi untuk membatalkan operasi.

// MainPage.xaml
...
<Button x:Name="workButton" Click="OnWork">Work</Button>
<Button x:Name="cancelButton" Click="OnCancel">Cancel</Button>
...

// MainPage.h
...
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Storage.Search.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Storage;
using namespace Windows::Storage::Search;
using namespace Microsoft::UI::Xaml;
...
struct MainPage : MainPageT<MainPage>
{
    MainPage()
    {
        InitializeComponent();
    }

    IAsyncAction OnWork(IInspectable /* sender */, RoutedEventArgs /* args */)
    {
        workButton().Content(winrt::box_value(L"Working..."));

        // Enable the Pictures Library capability in the app manifest file.
        StorageFolder picturesLibrary{ KnownFolders::PicturesLibrary() };

        m_async = picturesLibrary.GetFilesAsync(CommonFileQuery::OrderByDate, 0, 1000);

        IVectorView<StorageFile> filesInFolder{ co_await m_async };

        workButton().Content(box_value(L"Done!"));

        // Process the files in some way.
    }

    void OnCancel(IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
    {
        if (m_async.Status() != AsyncStatus::Completed)
        {
            m_async.Cancel();
            workButton().Content(winrt::box_value(L"Canceled"));
        }
    }

private:
    IAsyncOperation<::IVectorView<StorageFile>> m_async;
};
...

Untuk sisi implementasi pembatalan, mari kita mulai dengan contoh sederhana.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction ImplicitCancelationAsync()
{
    while (true)
    {
        std::cout << "ImplicitCancelationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto implicit_cancelation{ ImplicitCancelationAsync() };
    co_await 3s;
    implicit_cancelation.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

Jika Anda menjalankan contoh di atas, maka Anda akan melihat ImplicitCancelationAsync mencetak satu pesan per detik selama tiga detik, setelah itu secara otomatis berakhir sebagai akibat dari dibatalkan. Ini berfungsi karena, saat menemukan ekspresi co_await, korutin memeriksa apakah korutin tersebut telah dibatalkan. Jika ya, maka prosesnya langsung keluar; dan jika tidak, maka prosesnya ditangguhkan seperti biasa.

Pembatalan tentu saja dapat terjadi saat koroutine ditangguhkan. Hanya saat koroutine dilanjutkan kembali, atau mencapai co_await lain, barulah ia akan memeriksa pembatalan. Masalahnya adalah latensi yang berpotensi terlalu besar saat merespons pembatalan.

Jadi, opsi lain adalah secara eksplisit melakukan polling untuk pembatalan dari dalam koroutine Anda. Perbarui contoh di atas dengan kode dalam daftar di bawah ini. Dalam contoh baru ini, ExplicitCancelationAsync mengambil objek yang dikembalikan oleh fungsi winrt::get_cancellation_token , dan menggunakannya untuk memeriksa secara berkala apakah koroutin telah dibatalkan. Selama tidak dibatalkan, koroutine terus berulang tanpa henti; begitu dibatalkan, loop dan fungsi akan berakhir secara normal. Hasilnya sama dengan contoh sebelumnya, tetapi di sini keluar terjadi secara eksplisit, dan di bawah kontrol.

IAsyncAction ExplicitCancelationAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };

    while (!cancelation_token())
    {
        std::cout << "ExplicitCancelationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto explicit_cancelation{ ExplicitCancelationAsync() };
    co_await 3s;
    explicit_cancelation.Cancel();
}
...

Saat menunggu winrt::get_cancellation_token, Anda mendapatkan token pembatalan yang mengetahui IAsyncAction yang dihasilkan coroutine atas nama Anda. Anda dapat menggunakan operator pemanggilan fungsi pada token tersebut untuk memeriksa status pembatalan—yang pada dasarnya berarti memeriksa pembatalan secara berkala. Jika Anda melakukan beberapa operasi terikat komputasi, atau melakukan iterasi melalui koleksi besar, maka ini adalah teknik yang wajar.

Mendaftarkan panggilan balik pembatalan

Pembatalan pada Windows Runtime tidak secara otomatis diteruskan ke objek asinkron lainnya. Tetapi—diperkenalkan dalam versi 10.0.17763.0 (Windows 10, versi 1809) dari SDK Windows—Anda dapat mendaftarkan panggilan balik pembatalan. Ini adalah hook preemptif yang melaluinya pembatalan dapat diteruskan, dan memungkinkan integrasi dengan pustaka konkurensi yang ada.

Dalam contoh kode berikutnya ini, NestedCoroutineAsync melakukan pekerjaan, tetapi tidak memiliki logika pembatalan khusus di dalamnya. CancelationPropagatorAsync pada dasarnya adalah pembungkus pada coroutine berlapis; pembungkus meneruskan pembatalan terlebih dahulu.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction NestedCoroutineAsync()
{
    while (true)
    {
        std::cout << "NestedCoroutineAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction CancelationPropagatorAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };
    auto nested_coroutine{ NestedCoroutineAsync() };

    cancelation_token.callback([=]
    {
        nested_coroutine.Cancel();
    });

    co_await nested_coroutine;
}

IAsyncAction MainCoroutineAsync()
{
    auto cancelation_propagator{ CancelationPropagatorAsync() };
    co_await 3s;
    cancelation_propagator.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

CancelationPropagatorAsync mendaftarkan fungsi lambda untuk callback pembatalannya sendiri, lalu melakukan await (eksekusinya ditangguhkan) hingga operasi bertingkat tersebut selesai. Ketika atau jika CancellationPropagatorAsync dibatalkan, ia menyebarluaskan pembatalan ke coroutine berlapis. Tidak perlu melakukan polling untuk memeriksa pembatalan; dan pembatalan juga tidak akan terblokir tanpa batas waktu. Mekanisme ini cukup fleksibel untuk Anda gunakan guna melakukan interoperabilitas dengan pustaka coroutine atau konkurensi yang tidak mengetahui apa pun tentang C++/WinRT.

Pelaporan Kemajuan

Jika coroutine Anda mengembalikan IAsyncActionWithProgress atau IAsyncOperationWithProgress, Anda dapat memperoleh objek yang dikembalikan oleh fungsi winrt::get_progress_token, lalu menggunakannya untuk melaporkan progres kepada penangan progres. Berikut adalah contoh kode.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncOperationWithProgress<double, double> CalcPiTo5DPs()
{
    auto progress{ co_await winrt::get_progress_token() };

    co_await 1s;
    double pi_so_far{ 3.1 };
    progress.set_result(pi_so_far);
    progress(0.2);

    co_await 1s;
    pi_so_far += 4.e-2;
    progress.set_result(pi_so_far);
    progress(0.4);

    co_await 1s;
    pi_so_far += 1.e-3;
    progress.set_result(pi_so_far);
    progress(0.6);

    co_await 1s;
    pi_so_far += 5.e-4;
    progress.set_result(pi_so_far);
    progress(0.8);

    co_await 1s;
    pi_so_far += 9.e-5;
    progress.set_result(pi_so_far);
    progress(1.0);

    co_return pi_so_far;
}

IAsyncAction DoMath()
{
    auto async_op_with_progress{ CalcPiTo5DPs() };
    async_op_with_progress.Progress([](auto const& sender, double progress)
    {
        std::wcout << L"CalcPiTo5DPs() reports progress: " << progress << L". "
                   << L"Value so far: " << sender.GetResults() << std::endl;
    });
    double pi{ co_await async_op_with_progress };
    std::wcout << L"CalcPiTo5DPs() is complete !" << std::endl;
    std::wcout << L"Pi is approx.: " << pi << std::endl;
}

int main()
{
    winrt::init_apartment();
    DoMath().get();
}

Untuk melaporkan kemajuan, panggil token kemajuan dengan nilai kemajuan sebagai argumen. Untuk menetapkan hasil sementara, gunakan metode set_result() pada token progres.

Note

Melaporkan hasil provisi memerlukan C++/WinRT versi 2.0.210309.3 atau yang lebih baru.

Contoh di atas memilih untuk menetapkan hasil provisi untuk setiap laporan kemajuan. Anda dapat memilih untuk melaporkan hasil provisi kapan saja, jika sama sekali. Ini tidak perlu digabungkan dengan laporan kemajuan.

Note

Tidak tepat mengimplementasikan lebih dari satu penangan penyelesaian untuk aksi atau operasi asinkron. Anda dapat memiliki satu delegasi untuk acaranya yang telah selesai, atau Anda bisa co_await melakukannya. Jika Anda memiliki keduanya, maka yang kedua akan gagal. Salah satu dari dua jenis penangan penyelesaian berikut sesuai; bukan keduanya untuk objek asinkron yang sama.

auto async_op_with_progress{ CalcPiTo5DPs() };
async_op_with_progress.Completed([](auto const& sender, AsyncStatus /* status */)
{
    double pi{ sender.GetResults() };
});
auto async_op_with_progress{ CalcPiTo5DPs() };
double pi{ co_await async_op_with_progress };

Untuk informasi selengkapnya tentang handler penyelesaian, lihat Tipe delegasi untuk tindakan dan operasi asinkron.

Api dan lupakan

Terkadang, Anda memiliki tugas yang dapat dilakukan bersamaan dengan pekerjaan lain, dan Anda tidak perlu menunggu tugas tersebut selesai (tidak ada pekerjaan lain yang bergantung padanya), anda juga tidak memerlukannya untuk mengembalikan nilai. Dalam hal ini, Anda dapat melepaskan tugas dan melupakannya. Anda dapat melakukannya dengan menulis koroutine yang tipe kembaliannya adalah winrt::fire_and_forget (bukan salah satu tipe operasi asinkron Windows Runtime, atau concurrency::task).

// main.cpp
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace std::chrono_literals;

winrt::fire_and_forget CompleteInFiveSeconds()
{
    co_await 5s;
}

int main()
{
    winrt::init_apartment();
    CompleteInFiveSeconds();
    // Do other work here.
}

winrt::fire_and_forget juga berguna sebagai jenis pengembalian penanganan aktivitas Anda ketika Anda perlu melakukan operasi asinkron di dalamnya. Berikut adalah contoh (juga lihat Referensi yang kuat dan lemah di C++/WinRT).

winrt::fire_and_forget MyClass::MyMediaBinder_OnBinding(MediaBinder const&, MediaBindingEventArgs args)
{
    auto lifetime{ get_strong() }; // Prevent *this* from prematurely being destructed.
    auto ensure_completion{ unique_deferral(args.GetDeferral()) }; // Take a deferral, and ensure that we complete it.

    auto file{ co_await StorageFile::GetFileFromApplicationUriAsync(Uri(L"ms-appx:///video_file.mp4")) };
    args.SetStorageFile(file);

    // The destructor of unique_deferral completes the deferral here.
}

Argumen pertama ( pengirim) tidak dinamai, karena kami tidak pernah menggunakannya. Untuk alasan itu kita aman untuk meninggalkannya sebagai referensi. Tapi perhatikan bahwa args dikirimkan secara nilai. Lihat bagian Parameter-passing di atas.

Menunggu handel kernel

C++/WinRT menyediakan fungsi winrt::resume_on_signal , yang dapat Anda gunakan untuk menangguhkan hingga peristiwa kernel diberi sinyal. Anda bertanggung jawab untuk memastikan bahwa handle tetap valid hingga co_await resume_on_signal(h) Anda kembali. resume_on_signal sendiri tidak dapat melakukan hal itu untuk Anda, karena Anda mungkin telah kehilangan handle bahkan sebelum resume_on_signal mulai berjalan, seperti dalam contoh pertama ini.

IAsyncAction Async(HANDLE event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle is not valid here.
}

HANDLE yang diteruskan hanya valid hingga fungsi selesai, dan fungsi ini (yang merupakan korutin) selesai pada titik penangguhan pertama (yaitu co_await pertama dalam kasus ini). Saat menunggu DoWorkAsync, kontrol telah kembali ke pemanggil, frame pemanggil sudah keluar dari cakupan, dan Anda tidak lagi tahu apakah handle akan valid saat korutin Anda dilanjutkan kembali.

Secara teknis, coroutine kita menerima parameternya berdasarkan nilai, seperti yang seharusnya (lihat Parameter-passing di atas). Tetapi dalam hal ini kita perlu melangkah lebih jauh sehingga kita mengikuti semangat bimbingan itu (bukan hanya surat). Kita perlu meneruskan referensi kuat (dengan kata lain, kepemilikan) beserta handle. Berikut caranya.

IAsyncAction Async(winrt::handle event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle *is* valid here.
}

Meneruskan winrt::handle berdasarkan nilai memberikan semantik kepemilikan, yang memastikan bahwa handel kernel tetap valid sepanjang masa hidup coroutine.

Berikut cara Anda dapat memanggil coroutine tersebut.

namespace
{
    winrt::handle duplicate(winrt::handle const& other, DWORD access)
    {
        winrt::handle result;
        if (other)
        {
            winrt::check_bool(::DuplicateHandle(::GetCurrentProcess(),
		        other.get(), ::GetCurrentProcess(), result.put(), access, FALSE, 0));
        }
        return result;
    }

    winrt::handle make_manual_reset_event(bool initialState = false)
    {
        winrt::handle event{ ::CreateEvent(nullptr, true, initialState, nullptr) };
        winrt::check_bool(static_cast<bool>(event));
        return event;
    }
}

IAsyncAction SampleCaller()
{
    handle event{ make_manual_reset_event() };
    auto async{ Async(duplicate(event)) };

    ::SetEvent(event.get());
    event.close(); // Our handle is closed, but Async still has a valid handle.

    co_await async; // Will wake up when *event* is signaled.
}

Anda dapat meneruskan nilai batas waktu ke resume_on_signal, seperti dalam contoh ini.

winrt::handle event = ...

if (co_await winrt::resume_on_signal(event.get(), std::literals::2s))
{
    puts("signaled");
}
else
{
    puts("timed out");
}

Batas waktu asinkron menjadi mudah

C++/WinRT sangat bergantung pada coroutine C++. Efeknya pada penulisan kode konkurensi bersifat transformasional. Bagian ini membahas kasus ketika detail asinkronitas tidak penting, dan yang Anda inginkan hanyalah hasilnya saat itu juga. Untuk itu, implementasi C++/WinRT dari antarmuka operasi IAsyncAction Windows Runtime asinkron memiliki fungsi get, mirip dengan yang disediakan oleh std::future.

using namespace winrt::Windows::Foundation;
int main()
{
    IAsyncAction async = ...
    async.get();
    puts("Done!");
}

Fungsi get terblokir tanpa batas waktu hingga objek asinkron tersebut selesai. Objek asinkron cenderung berumur sangat pendek, jadi sering kali ini yang Anda butuhkan.

Namun, ada kalanya hal itu tidak cukup, dan Anda perlu menghentikan penantian setelah beberapa waktu berlalu. Menulis kode itu selalu dimungkinkan, berkat blok penyusun yang disediakan oleh Windows Runtime. Tetapi sekarang C++/WinRT membuatnya jauh lebih mudah dengan menyediakan fungsi wait_for . Ini juga diimplementasikan pada IAsyncAction, dan sekali lagi mirip dengan yang disediakan oleh std::future.

using namespace std::chrono_literals;
int main()
{
    IAsyncAction async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        puts("done");
    }
}

Note

wait_for menggunakan std::chrono::d uration di antarmuka, tetapi terbatas pada beberapa rentang yang lebih kecil dari apa yang disediakan std::chrono::d uration (kira-kira 49,7 hari).

wait_for dalam contoh berikutnya ini menunggu sekitar lima detik dan kemudian memeriksa penyelesaian. Jika perbandingannya menguntungkan, maka Anda tahu bahwa objek asinkron berhasil diselesaikan, dan Anda selesai. Jika Anda menunggu suatu hasil, Anda cukup melanjutkannya dengan memanggil metode GetResults untuk mendapatkan hasilnya.

Note

wait_for dan get tidak dapat digunakan bersamaan (Anda tidak dapat memanggil keduanya). Masing-masing dianggap sebagai penunggu, dan aksi/operasi asinkron Windows Runtime hanya mendukung satu penunggu.

int main()
{
    IAsyncOperation<int> async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        printf("result %d\n", async.GetResults());
    }
}

Karena objek asinkron telah selesai pada saat itu, metode GetResults segera mengembalikan hasilnya, tanpa menunggu lebih lanjut. Seperti yang Anda lihat, wait_for mengembalikan status objek asinkron. Jadi, Anda dapat menggunakannya untuk kontrol yang lebih halus, seperti ini.

switch (async.wait_for(5s))
{
case AsyncStatus::Completed:
    printf("result %d\n", async.GetResults());
    break;
case AsyncStatus::Canceled:
    puts("canceled");
    break;
case AsyncStatus::Error:
    puts("failed");
    break;
case AsyncStatus::Started:
    puts("still running");
    break;
}
  • Ingatlah bahwa AsyncStatus::Completed berarti bahwa objek asinkron berhasil diselesaikan, dan Anda dapat memanggil metode GetResults untuk mengambil hasil apa pun.
  • AsyncStatus::Canceled berarti objek asinkron dibatalkan. Pembatalan biasanya diminta oleh pemanggil, sehingga jarang menangani status ini. Biasanya, objek asinkron yang dibatalkan hanya dibuang. Anda dapat memanggil metode GetResults untuk melempar kembali pengecualian pembatalan jika diinginkan.
  • AsyncStatus::Error berarti objek asinkron telah gagal dalam beberapa cara. Anda dapat memanggil metode GetResults untuk melempar kembali pengecualian jika diinginkan.
  • AsyncStatus::Started berarti objek asinkron masih berjalan. Pola asinkron Windows Runtime tidak memungkinkan beberapa penantian, atau pelayan. Itu berarti bahwa Anda tidak dapat memanggil wait_for dalam perulangan. Jika waktu tunggu telah habis secara efektif, maka Anda dibiarkan dengan beberapa pilihan. Anda dapat meninggalkan objek, atau Anda dapat melakukan polling statusnya sebelum memanggil metode GetResults untuk mengambil hasil apa pun. Tapi yang terbaik adalah membuang objek pada saat ini.

Pola alternatif adalah hanya memeriksa Started, dan biarkan GetResults menangani kasus-kasus lainnya.

if (async.wait_for(5s) == AsyncStatus::Started)
{
    puts("timed out");
}
else
{
    // will throw appropriate exception if in canceled or error state
    auto results = async.GetResults();
}

Mengembalikan array secara asinkron

Di bawah ini adalah contoh MIDL 3.0 yang menghasilkan kesalahan MIDL2025: [msg]sintaks error [context]: expecting > or, near "[".

Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();

Alasannya adalah tidak valid untuk menggunakan array sebagai argumen jenis parameter ke antarmuka berparameter. Jadi kita membutuhkan cara yang kurang jelas untuk mencapai tujuan meneruskan array secara asinkron kembali dari metode kelas runtime.

Anda dapat mengembalikan array yang dikotak menjadi objek PropertyValue . Kode panggilan kemudian membuka kotaknya. Berikut adalah contoh kode yang dapat Anda coba dengan menambahkan kelas runtime SampleComponent ke proyek Windows Runtime Component (C++/WinRT), lalu menggunakannya dari (misalnya) proyek Blank App, Packaged (WinUI 3 in Desktop).

// SampleComponent.idl
namespace MyComponentProject
{
    runtimeclass SampleComponent
    {
        Windows.Foundation.IAsyncOperation<IInspectable> RetrieveCollectionAsync();
    };
}

// SampleComponent.h
...
struct SampleComponent : SampleComponentT<SampleComponent>
{
    ...
    Windows::Foundation::IAsyncOperation<Windows::Foundation::IInspectable> RetrieveCollectionAsync()
    {
        co_return Windows::Foundation::PropertyValue::CreateInt32Array({ 99, 101 }); // Box an array into a PropertyValue.
    }
}
...

// SampleCoreApp.cpp
...
MyComponentProject::SampleComponent m_sample_component;
...
auto boxed_array{ co_await m_sample_component.RetrieveCollectionAsync() };
auto property_value{ boxed_array.as<winrt::Windows::Foundation::IPropertyValue>() };
winrt::com_array<int32_t> my_array;
property_value.GetInt32Array(my_array); // Unbox back into an array.
...

API penting