Operasi konkurensi dan asinkron dengan C++/WinRT

Penting

Topik ini memperkenalkan konsep coroutines dan co_await, yang kami sarankan Anda gunakan di UI Anda dan dalam aplikasi non-UI Anda. Untuk kesederhanaan, sebagian besar contoh kode dalam topik pengantar ini menunjukkan proyek Windows Console Application (C ++/WinRT). Contoh kode selanjutnya dalam topik ini memang menggunakan coroutines, namun untuk kenyamanan contoh aplikasi konsol juga terus menggunakan blocking get function call sesaat sebelum keluar, sehingga aplikasi tidak keluar sebelum selesai mencetak outputnya. Anda tidak akan melakukan itu (memanggil fungsi blocking get ) dari utas UI. Sebagai gantinya, Anda akan menggunakan pernyataan tersebut co_await . Teknik yang akan Anda gunakan dalam aplikasi UI Anda dijelaskan dalam topik Konkurensi lanjutan dan asynchrony.

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

Operasi asinkron dan fungsi Windows Runtime "Async"

Setiap Windows Runtime API yang memiliki potensi untuk menyelesaikan lebih dari 50 milidetik diimplementasikan sebagai fungsi asinkron (dengan nama yang diakhiri dengan "Async"). Implementasi fungsi asinkron memulai pekerjaan pada utas lain, dan segera kembali dengan objek yang mewakili operasi asinkron. Ketika operasi asinkron selesai, objek yang dikembalikan itu berisi nilai apa pun yang dihasilkan dari pekerjaan. Ruang nama Runtime Windows Windows Foundation berisi empat jenis objek operasi asinkron.

Masing-masing jenis operasi asinkron ini diproyeksikan menjadi 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 penataan itu, Anda dapat menulis co_await pernyataan untuk secara kooperatif menunggu hasil dari fungsi apa pun yang mengembalikan salah satu jenis operasi asinkron ini. Dan Anda dapat menulis coroutines Anda sendiri yang mengembalikan jenis ini.

Contoh fungsi Windows asinkron adalah SyndicationClient::RetrieveFeedAsync, yang mengembalikan objek operasi asinkron dari tipe IAsyncOperationWithProgressTResult<, TProgress>.

Mari kita lihat beberapa cara — pemblokiran pertama, dan kemudian 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 tepat untuk aplikasi UI dibahas dalam Konkurensi lanjutan dan asynchrony.

Memblokir utas panggilan

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

Jika Anda ingin menyalin-tempel contoh ini langsung ke file kode sumber utama proyek Aplikasi Konsol Windows (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();
}

Panggilan mendapatkan membuat pengkodean yang nyaman, dan sangat ideal untuk aplikasi konsol atau utas latar belakang di mana Anda mungkin tidak ingin menggunakan coroutine karena alasan apa pun. Tapi itu tidak bersamaan atau asinkron, jadi tidak sesuai untuk utas UI (dan pernyataan akan menyala dalam build yang tidak optimal jika Anda mencoba menggunakannya pada satu). Untuk menghindari menahan thread OS dari melakukan pekerjaan lain yang berguna, kita perlu teknik yang berbeda.

Menulis coroutine

C ++/WinRT mengintegrasikan coroutine C ++ ke dalam model pemrograman untuk menyediakan cara alami untuk secara kooperatif menunggu hasilnya. Anda dapat menghasilkan operasi asinkron runtime Windows Anda sendiri dengan menulis coroutine. Dalam contoh kode di bawah ini, ProcessFeedAsync adalah coroutine.

Catatan

Fungsi get ada pada winrt tipe proyeksi C++/WinRT: Windows: Foundation: : IAsyncAction, sehingga Anda dapat memanggil fungsi dari dalam proyek C ++/ WinRT apa pun. Anda tidak akan menemukan fungsi yang tercantum sebagai anggota antarmuka IAsyncAction, karena get bukan bagian dari permukaan antarmuka biner aplikasi (ABI) dari IAsyncAction tipe runtime Windows 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.
}

Coroutine adalah fungsi yang dapat ditangguhkan dan dilanjutkan. Dalam coroutine ProcessFeedAsync di atas, ketika co_await pernyataan tercapai, coroutine secara asinkron memulai panggilan RetrieveFeedAsync dan kemudian segera menangguhkan dirinya sendiri dan mengembalikan kontrol kembali ke pemanggil (yang utama pada contoh di atas). utama kemudian dapat terus melakukan pekerjaan saat umpan sedang diambil dan dicetak. Setelah selesai (ketika panggilan RetrieveFeedAsync selesai), coroutine ProcessFeedAsync dilanjutkan pada pernyataan berikutnya.

Anda dapat menggabungkan coroutine ke coroutines lainnya. Atau Anda dapat menelepon untuk memblokir dan menunggu sampai selesai (dan mendapatkan hasilnya jika ada). Atau Anda dapat meneruskannya ke bahasa pemrograman lain yang mendukung runtime Windows.

Dimungkinkan juga untuk menangani peristiwa yang diselesaikan dan / atau kemajuan dari tindakan dan operasi asinkron 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, kami terus menggunakan panggilan fungsi blocking get sebelum keluar dari utama. Tapi itu hanya agar aplikasi tidak keluar sebelum selesai mencetak outputnya.

Mengembalikan tipe Runtime Windows secara asinkron

Dalam contoh berikutnya 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());
}

Pada contoh di atas, RetrieveBlogFeedAsync mengembalikan IAsyncOperationWithProgress, yang memiliki kemajuan dan nilai pengembalian. Kita dapat melakukan pekerjaan lain sementara RetrieveBlogFeedAsync melakukan tugasnya dan mengambil umpan. Kemudian, kami memanggil objek operasi asinkron untuk memblokir, menunggu sampai selesai, dan kemudian mendapatkan hasil operasi.

Jika Anda secara asinkron mengembalikan jenis Runtime Windows, maka Anda harus mengembalikan IAsyncOperationTResult<> atau IAsyncOperationWithProgressTResult<, TProgress>. Setiap kelas runtime pihak pertama atau ketiga memenuhi syarat, atau jenis apa pun yang dapat diteruskan ke atau dari fungsi Runtime Windows (misalnya, , intatau winrt::hstring). Kompiler akan membantu Anda dengan kesalahan "T harus tipe WinRT" jika Anda mencoba menggunakan salah satu jenis operasi asinkron ini dengan jenis Runtime non-Windows.

Jika coroutine tidak memiliki setidaknya satu co_await pernyataan maka, untuk memenuhi syarat sebagai coroutine, ia harus memiliki setidaknya satu co_return atau satu co_yield pernyataan. Akan ada kasus di mana coroutine Anda dapat mengembalikan nilai tanpa memperkenalkan asynchrony, dan karena itu tanpa memblokir atau beralih konteks. Berikut adalah contoh yang melakukan itu (saat kedua dan berikutnya disebut) dengan caching nilai.

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 secara asinkron mengembalikan tipe yang bukan tipe Runtime Windows, maka Anda harus mengembalikan konkurensi Pustaka Pola Paralel (PPL) ::task. Kami merekomendasikan konkurensi::task karena memberi Anda kinerja yang lebih baik (dan kompatibilitas yang lebih baik ke depan) daripada std::future .

Tip

Jika Anda menyertakan <pplawait.h>, maka Anda dapat menggunakan konkurensi::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;
}

Parameter-passing

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

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

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

// 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 sinkron sampai titik suspensi pertama, di mana kontrol dikembalikan ke penelepon dan bingkai panggilan keluar dari ruang lingkup. Pada saat coroutine dilanjutkan, apa pun mungkin terjadi pada nilai sumber yang dirujuk oleh parameter referensi. Dari perspektif coroutine, parameter referensi memiliki masa hidup yang tidak terkendali. Jadi, dalam contoh di atas, kita aman untuk mengakses nilai sampai co_await, tetapi tidak setelah itu. Jika nilai itu dihancurkan oleh penelepon, mencoba mengaksesnya di dalam coroutine maka menghasilkan kerusakan memori. Kami juga tidak dapat dengan aman meneruskan nilai ke DoOtherWorkAsync jika ada risiko bahwa fungsi itu pada gilirannya akan ditangguhkan dan kemudian mencoba menggunakan nilai setelah dilanjutkan.

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&

Passing by value mengharuskan argumen tidak mahal untuk dipindahkan atau disalin; dan itu biasanya terjadi untuk pointer cerdas.

Ini juga bisa diperdebatkan bahwa (kecuali jika Anda ingin memindahkan nilai) melewati nilai const adalah praktik yang baik. Ini tidak akan berpengaruh pada nilai sumber dari mana Anda membuat salinan, tetapi itu membuat niatnya jelas, dan membantu jika Anda secara tidak sengaja memodifikasi salinannya.

// 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 tanda tangan coroutine Anda, tetapi Anda dapat mengubah implementasinya, maka Anda dapat membuat salinan lokal sebelum yang pertama co_await.

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 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 penunjuk ini dengan aman di coroutine anggota kelas

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

API Penting