Konkurensi dan asinkron tingkat lanjut dengan C++/WinRT

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

Untuk pengenalan subjek ini, pertama-tama baca operasi Konkurensi dan asinkron.

Pekerjaan offloading ke kumpulan utas Windows

Koroutine adalah fungsi seperti yang lain di mana penelepon diblokir sampai fungsi mengembalikan eksekusi ke dalamnya. Dan, kesempatan pertama bagi koroutine untuk kembali adalah yang pertama co_await, , co_returnatau co_yield.

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_awaitfungsi 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 membongkar beberapa pekerjaan ke kumpulan utas, tetapi kemudian Anda 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 melempar pengecualian winrt::hresult_wrong_thread, karena TextBlock harus diperbarui dari utas yang membuatnya, yang merupakan 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.
}

Selama coroutine di atas dipanggil dari utas UI yang membuat TextBlock, maka teknik ini berfungsi. Akan ada banyak kasus di aplikasi tempat Anda yakin akan hal itu.

Untuk solusi yang lebih umum untuk memperbarui UI, yang mencakup kasus di mana Anda tidak yakin tentang utas panggilan, Anda dapat co_awaitfungsi winrt::resume_foreground untuk beralih ke utas latar depan tertentu. Dalam contoh kode di bawah ini, kami menentukan utas latar depan dengan meneruskan objek dispatcher yang terkait dengan TextBlock (dengan mengakses properti Dispatcher-nya). Implementasi winrt::resume_foreground memanggil CoreDispatcher.RunAsync pada objek 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.Dispatcher());

    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 koroutine, utas asli eksekusi dapat hilang dan dimulainya kembali dapat terjadi pada utas apa pun (dengan kata lain, utas apa pun dapat memanggil metode Selesai untuk operasi asinkron).

Tetapi jika Anda co_await salah satu dari empat jenis operasi asinkron Windows Runtime (IAsyncXxx), maka C++/WinRT menangkap konteks panggilan pada saat Anda co_await. Dan itu memastikan bahwa Anda masih dalam konteks tersebut ketika kelanjutan dilanjutkan. 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 karena C++/WinRT menyediakan kode untuk menyesuaikan jenis operasi asinkron Windows Runtime tersebut dengan dukungan bahasa koroutine C++ (bagian kode ini disebut adaptor tunggu). Jenis yang dapat ditunggu yang tersisa di C++/WinRT hanyalah pembungkus kumpulan utas dan/atau pembantu; sehingga mereka lengkap pada kumpulan utas.

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 beberapa jenis lain—bahkan dalam implementasi koroutine C++/WinRT—maka pustaka lain menyediakan adaptor, dan Anda harus memahami apa yang dilakukan adaptor tersebut dalam hal dimulainya kembali dan konteks.

Untuk menjaga konteks tetap turun ke minimum, Anda dapat menggunakan beberapa teknik yang telah kita lihat dalam topik ini. Mari kita lihat beberapa ilustrasi melakukan itu. Dalam contoh kode pseudo berikutnya ini, kami menunjukkan kerangka penanganan aktivitas yang memanggil API Runtime Windows untuk memuat gambar, turun 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->Dispatcher());

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

    // Display the image in the UI.
}

Untuk skenario ini, ada sedikit ineffiensi di sekitar panggilan ke StorageFile::OpenAsync. Ada pengalihan konteks yang diperlukan ke utas latar belakang (sehingga handler dapat mengembalikan eksekusi ke pemanggil), saat dimulai kembali setelah C++/WinRT memulihkan konteks utas UI. Tapi, dalam hal ini, tidak perlu berada di utas UI sampai kita akan memperbarui UI. Semakin banyak API Runtime Windows yang kami panggil sebelum panggilan kami untuk winrt::resume_background, semakin banyak switch konteks bolak-balik yang tidak perlu yang kami timbulkan. Solusinya adalah tidak memanggil API Windows Runtime sebelum itu. Pindahkan semuanya 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->Dispatcher());

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

    // Display the image in the UI.
}

Jika Anda ingin melakukan sesuatu yang lebih canggih, maka Anda dapat menulis adaptor tunggu Anda sendiri. Misalnya, jika Anda ingin co_await melanjutkan pada utas yang sama dengan yang diselesaikan oleh tindakan asinkron (jadi, tidak ada sakelar konteks), maka Anda dapat memulai dengan menulis adaptor tunggu yang mirip dengan yang ditunjukkan di bawah ini.

Catatan

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 di basis kode Anda sendiri, kami sarankan Anda mengembangkan dan menguji struct adaptor tunggu 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 tunggu no_switch , Anda harus terlebih dahulu mengetahui bahwa ketika pengkompilasi C++ menemukan ekspresi yang co_await dicarinya 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 no_switch tunggu, cukup ubah jenis co_await ekspresi 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 Anda dapat mengandalkan unwinding tumpukan dan antrean ulang yang terjadi; dan 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. Stack-unwinding bisa sangat penting.

Fungsi winrt::resume_foreground juga awalnya hanya mendukung CoreDispatcher (terkait dengan CoreWindow), yang diperkenalkan sebelum Windows 10. Kami telah memperkenalkan dispatcher yang lebih fleksibel dan efisien: DispatcherQueue. 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 (terkandung dalam pengontrol) pada utas privat, lalu meneruskan pengontrol ke coroutine. Coroutine dapat menggunakan antrean untuk menunggu (menangguhkan dan melanjutkan) pada 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. Ini juga tepat bagaimana Anda dapat mendukung antrean yang efisien dan mulus pada aplikasi desktop Win32 gaya Petzold yang 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 koroutin yang elegan untuk alur kerja berbasis asinkron atau pesan 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 secara opsional mengatur prioritas resumption.

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;
 
    ...
}

Catatan

Seperti yang ditunjukkan di atas, pastikan untuk menyertakan header proyeksi untuk namespace jenis yang Anda co_await-ing. Misalnya, Windows::UI::Core::CoreDispatcher, Windows::System::D ispatcherQueue, atau Microsoft::UI::D ispatching::D ispatcherQueue.

Atau, dalam hal ini mendeteksi pematian antrean, dan menanganinya dengan anggun.

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 dimulainya kembali akan terjadi pada utas dispatcher. Dengan kata lain, antrean itu berhasil. Sebaliknya, itu kembali false untuk menunjukkan bahwa eksekusi tetap pada utas panggilan karena pengontrol antrean dimatikan dan tidak lagi melayani permintaan antrean.

Jadi, Anda memiliki banyak kekuatan di ujung jari Anda ketika Anda menggabungkan C++/WinRT dengan coroutines; dan terutama ketika melakukan beberapa pengembangan aplikasi desktop bergaya Petzold sekolah lama.

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 Windows::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, pada mengalami co_await ekspresi, koroutine memeriksa apakah telah dibatalkan. Jika sudah, maka sirkuit pendek keluar; dan jika belum, maka ia menangguhkan seperti biasa.

Pembatalan tentu saja dapat terjadi saat coroutine ditangguhkan. Hanya ketika koroutine dilanjutkan, atau mengenai yang lain co_await, apakah akan memeriksa pembatalan. Masalah ini adalah salah satu latensi yang berpotensi terlalu kasar dalam menanggapi pembatalan.

Jadi, opsi lain adalah secara eksplisit melakukan polling untuk pembatalan dari dalam coroutine 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, perulangan koroutine tidak terbatas; setelah dibatalkan, perulangan dan fungsi keluar 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();
}
...

Menunggu winrt::get_cancellation_token mengambil token pembatalan dengan pengetahuan tentang IAsyncAction bahwa coroutine diproduksi atas nama Anda. Anda dapat menggunakan operator panggilan fungsi pada token tersebut untuk mengkueri status pembatalan—pada dasarnya polling untuk pembatalan. Jika Anda melakukan beberapa operasi terikat komputasi, atau melakukan iterasi melalui koleksi besar, maka ini adalah teknik yang wajar.

Mendaftarkan panggilan balik pembatalan

Pembatalan Windows Runtime tidak secara otomatis mengalir ke objek asinkron lainnya. Tetapi—diperkenalkan dalam versi 10.0.17763.0 (Windows 10, versi 1809) dari Windows SDK—Anda dapat mendaftarkan panggilan balik pembatalan. Ini adalah kait pra-emptive di mana pembatalan dapat disebarluaskan, dan memungkinkan untuk berintegrasi 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 panggilan balik pembatalannya sendiri, dan kemudian menunggu (ditangguhkan) sampai pekerjaan berlapis selesai. Ketika atau jika CancellationPropagatorAsync dibatalkan, ia menyebarluaskan pembatalan ke koroutine berlapis. Tidak perlu melakukan polling untuk pembatalan; atau pembatalan diblokir tanpa batas waktu. Mekanisme ini cukup fleksibel bagi Anda untuk menggunakannya untuk menginteropsi pustaka koroutine atau konkurensi yang tidak mengenal C++/WinRT.

Kemajuan pelaporan

Jika coroutine Anda mengembalikan IAsyncActionWithProgress, atau IAsyncOperationWithProgress, maka Anda dapat mengambil objek yang dikembalikan oleh fungsi winrt::get_progress_token , dan menggunakannya untuk melaporkan kemajuan kembali ke handler kemajuan. 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 mengatur hasil provisi, gunakan set_result() metode pada token kemajuan.

Catatan

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.

Catatan

Tidak benar untuk mengimplementasikan lebih dari satu handler penyelesaian untuk tindakan 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 penangan penyelesaian, lihat Mendelegasikan jenis 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 koroutin yang jenis pengembaliannya adalah winrt::fire_and_forget (bukan salah satu jenis operasi asinkron Windows Runtime, atau konkurensi::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 amati bahwa args diteruskan oleh 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 handel tetap valid hingga Anda co_await resume_on_signal(h) kembali. resume_on_signal sendiri tidak dapat melakukannya untuk Anda, karena Anda mungkin telah kehilangan handel bahkan sebelum resume_on_signal dimulai, 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 masuk hanya valid sampai fungsi kembali, dan fungsi ini (yang merupakan koroutine) kembali pada titik suspensi pertama (yang pertama co_await dalam kasus ini). Saat menunggu DoWorkAsync, kontrol telah kembali ke pemanggil, bingkai panggilan telah keluar dari cakupan, dan Anda tidak lagi tahu apakah handel akan valid ketika koroutine Anda dilanjutkan.

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 yang kuat (dengan kata lain, kepemilikan) bersama dengan handel. 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 by value memberikan semantik kepemilikan, yang memastikan bahwa handel kernel tetap berlaku untuk masa pakai koroutine.

Berikut adalah bagaimana Anda mungkin memanggil koroutine itu.

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 banyak diinvestasikan dalam koroutin C++. Efeknya pada penulisan kode konkurensi bersifat transformasional. Bagian ini membahas kasus di mana detail asinkron tidak penting, dan yang Anda inginkan adalah hasilnya di sana dan kemudian. Untuk alasan itu, implementasi C++/WinRT dari antarmuka operasi asinkron IAsyncAction Windows Runtime memiliki fungsi get, mirip dengan yang disediakan oleh std::future.

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

Blok fungsi get tidak terbatas, sementara objek asinkron selesai. Objek asinkron cenderung berumur sangat pendek, jadi sering kali ini yang Anda butuhkan.

Tetapi ada kasus di mana itu tidak cukup, dan Anda perlu meninggalkan tunggu 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");
    }
}

Catatan

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 beberapa hasil, maka Anda cukup mengikutinya dengan panggilan ke metode GetResults untuk mengambil hasilnya.

Catatan

wait_for dan dapatkan saling eksklusif (Anda tidak dapat memanggil keduanya). Mereka masing-masing dihitung sebagai pelayan, dan tindakan/operasi asinkron Windows Runtime hanya mendukung satu pelayan.

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 menumbuhkan kembali pengecualian pembatalan jika Anda mau.
  • AsyncStatus::Error berarti objek asinkron telah gagal dalam beberapa cara. Anda dapat memanggil metode GetResults untuk menumbuhkan kembali pengecualian jika Anda mau.
  • AsyncStatus::Started berarti objek asinkron masih berjalan. Pola asinkron Windows Runtime tidak mengizinkan beberapa tunggu, 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 memeriksa hanya untuk Memulai, dan membiarkan GetResults menangani kasus lain.

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 mengonsumsinya dari (misalnya) proyek Aplikasi Inti (C++/WinRT).

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