Operasi konkurensi dan asinkron dengan C++/WinRT

Important

Topik ini memperkenalkan konsep koroutin dan co_await, yang kami sarankan Anda gunakan di UI Anda dan di aplikasi non-UI Anda. Untuk kesederhanaan, sebagian besar contoh kode dalam topik pengantar ini menunjukkan proyek Aplikasi Konsol Windows (C++/WinRT). Contoh kode berikutnya dalam topik ini memang menggunakan coroutine, tetapi demi kemudahan, contoh aplikasi konsol juga tetap menggunakan panggilan fungsi get yang memblokir tepat sebelum aplikasi berakhir, agar aplikasi tidak berakhir sebelum selesai mencetak outputnya. Anda tidak boleh melakukan itu (memanggil fungsi get yang memblokir) pada utas UI. Sebagai gantinya, Anda akan menggunakan pernyataan tersebut co_await . Teknik yang akan Anda gunakan dalam aplikasi UI dijelaskan dalam topik Konkurensi tingkat lanjut dan asinkron.

Topik pengantar ini menunjukkan beberapa cara di mana Anda dapat membuat dan mengonsumsi objek asinkron Windows Runtime dengan C++/WinRT. Setelah membaca topik ini, terutama untuk teknik yang akan Anda gunakan dalam aplikasi UI Anda, lihat juga Konkurensi dan asinkron tingkat lanjut.

Operasi asinkron dan fungsi "Async" Windows Runtime

API Windows Runtime apa pun yang berpotensi memakan waktu lebih dari 50 milidetik untuk diselesaikan diimplementasikan sebagai fungsi asinkron (dengan nama yang berakhiran "Asinkron"). Implementasi fungsi asinkron memulai pekerjaan di utas lain dan langsung mengembalikan objek yang mewakili operasi asinkron. Ketika operasi asinkron selesai, objek yang dikembalikan berisi nilai apa pun yang dihasilkan dari pekerjaan. Namespace Windows Runtime Windows::Foundation berisi empat jenis objek operasi asinkron.

Masing-masing jenis operasi asinkron ini diproyeksikan ke dalam jenis yang sesuai di namespace winrt::Windows::Foundation C++/WinRT. C++/WinRT juga berisi struktur adaptor tunggu internal. Anda tidak menggunakannya secara langsung tetapi, berkat struktur itu, Anda dapat menulis co_await pernyataan untuk secara kooperatif menunggu hasil fungsi apa pun yang mengembalikan salah satu jenis operasi asinkron ini. Dan Anda dapat menulis korutin Anda sendiri yang mengembalikan tipe-tipe ini.

Contoh fungsi Windows asinkron adalah SyndicationClient::RetrieveFeedAsync, yang mengembalikan objek operasi asinkron jenis IAsyncOperationWithProgress<TResult, TProgress>.

Mari kita lihat beberapa cara—pemblokiran pertama, lalu non-pemblokiran—menggunakan C++/WinRT untuk memanggil API seperti itu. Hanya untuk ilustrasi ide-ide dasar, kita akan menggunakan proyek Aplikasi Konsol Windows (C++/WinRT) dalam beberapa contoh kode berikutnya. Teknik yang lebih sesuai untuk aplikasi UI dibahas dalam Konkurensi tingkat lanjut dan asinkron.

Memblokir utas panggilan

Contoh kode di bawah ini menerima objek operasi asinkron dari RetrieveFeedAsync, dan memanggil masuk ke objek tersebut untuk memblokir utas panggilan sampai hasil operasi asinkron tersedia.

Jika Anda ingin menyalin-tempel contoh ini langsung ke file kode sumber utama proyek Windows Console Application (C++/WinRT), maka pertama-tama atur Tidak Menggunakan Header yang Telah Dikompilasi di properti proyek.

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void ProcessFeed()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
    // use syndicationFeed.
}

int main()
{
    winrt::init_apartment();
    ProcessFeed();
}

Memanggil get memudahkan penulisan kode, dan ini sangat ideal untuk aplikasi konsol atau utas latar belakang saat Anda mungkin tidak ingin menggunakan coroutine, apa pun alasannya. Tetapi ini tidak berjalan secara konkuren maupun asinkron, jadi tidak cocok untuk utas UI (dan assertion akan terpicu dalam build yang belum dioptimalkan jika Anda mencoba menggunakannya pada utas tersebut). Untuk menghindari utas OS tertahan sehingga tidak dapat mengerjakan tugas berguna lainnya, kita memerlukan teknik yang berbeda.

Menulis koroutine

C++/WinRT mengintegrasikan koroutin C++ ke dalam model pemrograman untuk memberikan cara alami untuk menunggu hasil secara kooperatif. Anda dapat membuat operasi asinkron Windows Runtime Anda sendiri dengan menulis coroutine. Dalam contoh kode di bawah ini, ProcessFeedAsync adalah coroutine.

Note

Fungsi get ada pada jenis proyeksi C++/WinRT winrt::Windows::Foundation::IAsyncAction, sehingga Anda dapat memanggil fungsi dari dalam proyek C++/WinRT apa pun. Anda tidak akan menemukan fungsi yang terdaftar sebagai anggota antarmuka IAsyncAction, karena get bukan bagian dari permukaan antarmuka biner aplikasi (ABI) dari jenis Windows Runtime IAsyncAction yang sebenarnya.

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

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

int main()
{
    winrt::init_apartment();

    auto processOp{ ProcessFeedAsync() };
    // do other work while the feed is being printed.
    processOp.get(); // no more work to do; call get() so that we see the printout before the application exits.
}

Koroutine adalah fungsi yang dapat ditangguhkan dan dilanjutkan. Dalam korutin ProcessFeedAsync di atas, ketika eksekusi mencapai pernyataan co_await, korutin tersebut secara asinkron memulai pemanggilan RetrieveFeedAsync, lalu segera menangguhkan dirinya sendiri dan mengembalikan kontrol kepada pemanggil (yang dalam contoh di atas adalah main). main kemudian dapat terus melakukan tugas lain sementara feed sedang diambil dan dicetak. Setelah selesai (ketika panggilan RetrieveFeedAsync selesai), coroutine ProcessFeedAsync dilanjutkan pada pernyataan berikutnya.

Anda dapat mengagregasi koroutine ke koroutin lain. Atau Anda dapat memanggil get untuk memblokir dan menunggunya hingga selesai (dan mendapatkan hasilnya, jika ada). Atau Anda dapat meneruskannya ke bahasa pemrograman lain yang mendukung Windows Runtime.

Dimungkinkan juga untuk menangani peristiwa tindakan dan operasi asinkron yang telah selesai dan/atau kemajuan dengan menggunakan delegasi. Untuk detail, dan contoh kode, lihat Mendelegasikan jenis untuk tindakan dan operasi asinkron.

Seperti yang Anda lihat, dalam contoh kode di atas, kita tetap menggunakan panggilan fungsi get yang memblokir tepat sebelum keluar dari main. Tetapi itu hanya agar aplikasi tidak keluar sebelum menyelesaikan pencetakan outputnya.

Mengembalikan tipe Windows Runtime secara asinkron

Dalam contoh berikutnya ini kita membungkus panggilan ke RetrieveFeedAsync, untuk URI tertentu, untuk memberi kita fungsi RetrieveBlogFeedAsync yang secara asinkron mengembalikan SyndicationFeed.

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

IAsyncOperationWithProgress<SyndicationFeed, RetrievalProgress> RetrieveBlogFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    return syndicationClient.RetrieveFeedAsync(rssFeedUri);
}

int main()
{
    winrt::init_apartment();

    auto feedOp{ RetrieveBlogFeedAsync() };
    // do other work.
    PrintFeed(feedOp.get());
}

Dalam contoh di atas, RetrieveBlogFeedAsync mengembalikan IAsyncOperationWithProgress, yang memiliki kemajuan dan nilai pengembalian. Kita dapat melakukan pekerjaan lain saat RetrieveBlogFeedAsync melakukan hal itu dan mengambil umpan. Kemudian, kita memanggil dapatkan objek operasi asinkron itu untuk memblokir, menunggunya selesai, dan kemudian mendapatkan hasil operasi.

Jika Anda mengembalikan tipe Windows Runtime secara asinkron, Anda harus mengembalikan IAsyncOperation<TResult> atau IAsyncOperationWithProgress<TResult, TProgress>. Setiap kelas runtime pihak pertama maupun pihak ketiga dianggap memenuhi syarat, begitu pula tipe apa pun yang dapat diteruskan ke fungsi Windows Runtime atau dikembalikan darinya (misalnya, int, atau winrt::hstring). Kompiler akan menampilkan error "T harus berupa tipe WinRT" jika Anda mencoba menggunakan salah satu tipe operasi asinkron ini dengan tipe yang bukan Windows Runtime.

Jika sebuah korutin tidak memiliki setidaknya satu pernyataan co_await, maka agar dianggap sebagai korutin, korutin tersebut harus memiliki setidaknya satu pernyataan co_return atau satu pernyataan co_yield. Akan ada kasus ketika coroutine Anda dapat mengembalikan nilai tanpa melibatkan operasi asinkron apa pun, sehingga tidak memblokir maupun berpindah konteks. Berikut adalah contoh yang melakukannya (pada pemanggilan kedua dan seterusnya) dengan menyimpan nilai dalam cache.

winrt::hstring m_cache;

IAsyncOperation<winrt::hstring> ReadAsync()
{
    if (m_cache.empty())
    {
        // Asynchronously download and cache the string.
    }
    co_return m_cache;
}

Mengembalikan tipe non-Windows-Runtime secara asinkron

Jika Anda mengembalikan secara asinkron sebuah tipe yang bukan tipe Windows Runtime, Anda harus mengembalikan concurrency::task dari Parallel Patterns Library (PPL). Kami merekomendasikan concurrency::task karena memberikan performa yang lebih baik kepada Anda (dan kompatibilitas yang lebih baik untuk ke depannya) dibandingkan dengan std::future.

Tip

Jika Anda menyertakan <pplawait.h>, maka Anda dapat menggunakan concurrency::task sebagai tipe coroutine.

// main.cpp
#include <iostream>
#include <ppltasks.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

concurrency::task<std::wstring> RetrieveFirstTitleAsync()
{
    return concurrency::create_task([]
        {
            Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
            SyndicationClient syndicationClient;
            SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
            return std::wstring{ syndicationFeed.Items().GetAt(0).Title().Text() };
        });
}

int main()
{
    winrt::init_apartment();

    auto firstTitleOp{ RetrieveFirstTitleAsync() };
    // Do other work here.
    std::wcout << firstTitleOp.get() << std::endl;
}

Pengoperasian parameter

Untuk fungsi sinkron, Anda harus menggunakan const& parameter secara default. Itu akan menghindari overhead salinan (yang melibatkan penghitungan referensi, dan itu berarti kenaikan dan penurunan yang saling mengunci).

// Synchronous function.
void DoWork(Param const& value);

Tetapi Anda dapat mengalami masalah jika Anda meneruskan parameter referensi ke koroutine.

// NOT the recommended way to pass a value to a coroutine!
IASyncAction DoWorkAsync(Param const& value)
{
    // While it's ok to access value here...

    co_await DoOtherWorkAsync(); // (this is the first suspension point)...

    // ...accessing value here carries no guarantees of safety.
}

Dalam coroutine, eksekusi berjalan secara sinkron hingga mencapai titik penangguhan pertama, saat kendali dikembalikan kepada pemanggil dan frame pemanggil keluar dari scope. Pada saat coroutine dilanjutkan, apa pun mungkin terjadi pada nilai sumber yang direferensikan parameter referensi. Dari sudut pandang coroutine, parameter referensi memiliki masa hidup yang tidak terkontrol. Jadi, dalam contoh di atas, kita aman untuk mengakses nilai hingga co_await, tetapi tidak setelahnya. Apabila nilai dihancurkan oleh pemanggil, upaya untuk mengaksesnya di dalam koroutine setelahnya akan mengakibatkan kerusakan memori. Kita juga tidak dapat dengan aman meneruskan nilai ke DoOtherWorkAsync jika ada risiko bahwa fungsi tersebut kemudian akan ditangguhkan lalu mencoba menggunakan nilai setelah dilanjutkan kembali.

Untuk membuat parameter aman digunakan setelah menangguhkan dan melanjutkan, coroutine Anda harus menggunakan pass-by-value secara default untuk memastikan bahwa mereka menangkap berdasarkan nilai, dan menghindari masalah seumur hidup. Kasus ketika Anda dapat menyimpang dari panduan itu karena Anda yakin bahwa aman untuk melakukannya akan jarang terjadi.

// Coroutine
IASyncAction DoWorkAsync(Param value); // not const&

Meneruskan argumen secara by value mengharuskan argumen dapat dipindahkan atau disalin dengan biaya rendah; dan hal itu biasanya berlaku untuk smart pointer.

Juga dapat dibilang bahwa (kecuali Anda ingin memindahkan nilai) melewati nilai const adalah praktik yang baik. Ini tidak akan berpengaruh pada nilai sumber tempat Anda membuat salinan, tetapi membuat niat menjadi jelas, dan membantu jika Anda secara tidak sengaja memodifikasi salinan.

// coroutine with strictly unnecessary const (but arguably good practice).
IASyncAction DoWorkAsync(Param const value);

Lihat juga Array dan vektor standar, yang berkaitan dengan cara meneruskan vektor standar ke callee asinkron.

Jika Anda tidak dapat mengubah signatur coroutine Anda, tetapi dapat mengubah implementasinya, Anda dapat membuat salinan lokal sebelum co_await pertama.

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_value = value;
    // It's ok to access both safe_value and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_value here (not value).
}

Jika Param mahal untuk disalin, maka ekstrak hanya potongan-potongan yang Anda butuhkan sebelum yang pertama co_await.

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_data = value.data;
    // It's ok to access safe_data, value.data, and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_data here (not value.data, nor value).
}

Mengakses pointer ini dengan aman dalam koroutine anggota kelas

Lihat Referensi yang kuat dan lemah di C++/WinRT.

API penting