Bagikan melalui


Operasi konkurensi dan asinkron dengan C++/WinRT

Penting

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 selanjutnya dalam topik ini memang menggunakan coroutines, tetapi untuk kenyamanan contoh aplikasi konsol juga terus menggunakan panggilan fungsi get pemblokiran tepat sebelum keluar, sehingga aplikasi tidak keluar sebelum menyelesaikan pencetakan outputnya. Anda tidak akan melakukan itu (panggil fungsi dapatkan pemblokiran) dari 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 menggunakan 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 "Asinkron" Windows Runtime

Api Runtime Windows apa pun yang berpotensi membutuhkan waktu lebih dari 50 milidetik untuk diselesaikan diimplementasikan sebagai fungsi asinkron (dengan nama yang berakhiran "Asinkron"). Implementasi fungsi asinkron memulai pekerjaan pada utas lain, dan segera kembali dengan objek yang mewakili operasi asinkron. Ketika operasi asinkron selesai, objek yang dikembalikan berisi nilai apa pun yang dihasilkan dari pekerjaan. Namespace Windows::Foundation Windows Runtime 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 koroutine Anda sendiri yang mengembalikan jenis 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 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();
}

Memanggil get 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. Tetapi tidak bersamaan atau asinkron, jadi tidak sesuai untuk utas UI (dan pernyataan akan diaktifkan dalam build yang tidak optimal jika Anda mencoba menggunakannya pada satu). Untuk menghindari menahan utas OS dari melakukan pekerjaan 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 menghasilkan operasi asinkron Windows Runtime Anda sendiri dengan menulis koroutine. Dalam contoh kode di bawah ini, ProcessFeedAsync adalah coroutine.

Catatan

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 koroutin 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 dalam 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 mengagregasi koroutine ke koroutin lain. Atau Anda dapat memanggil untuk memblokir dan menunggunya selesai (dan mendapatkan hasilnya jika ada). Atau Anda dapat meneruskannya ke bahasa pemrograman lain yang mendukung Windows Runtime.

Dimungkinkan juga untuk menangani peristiwa dan/atau kemajuan tindakan dan operasi asinkron yang diselesaikan 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 get pemblokiran tepat sebelum keluar dari utama. Tetapi itu hanya agar aplikasi tidak keluar sebelum menyelesaikan pencetakan outputnya.

Mengembalikan jenis Runtime Windows 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 jenis Windows Runtime secara asinkron, maka Anda harus mengembalikan TResult> IAsyncOperation<atau IAsyncOperationWithProgress<TResult, TProgress>. Setiap kelas runtime pihak pertama atau ketiga memenuhi syarat, atau jenis apa pun yang dapat diteruskan ke atau dari fungsi Windows Runtime (misalnya, int, atau winrt::hstring). Pengompilasi akan membantu Anda dengan kesalahan "T harus jenis WinRT" jika Anda mencoba menggunakan salah satu jenis operasi asinkron ini dengan jenis Runtime non-Windows.

Jika koroutine tidak memiliki setidaknya satu co_await pernyataan maka, untuk memenuhi syarat sebagai koroutin, itu harus memiliki setidaknya satu co_return atau satu co_yield pernyataan. Akan ada kasus di mana coroutine Anda dapat mengembalikan nilai tanpa memperkenalkan asinkron apa pun, dan oleh karena itu tanpa memblokir atau mengalihkan konteks. Berikut adalah contoh yang melakukan itu (kali kedua dan berikutnya dipanggil) dengan penembolokan nilai.

winrt::hstring m_cache;

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

Mengembalikan jenis non-Windows-Runtime secara asinkron

Jika Anda secara asinkron mengembalikan jenis yang bukan jenis Runtime Windows, maka Anda harus mengembalikan konkurensi Pustaka Pola Paralel (PPL)::task. Kami merekomendasikan konkurensi::task karena memberi Anda performa yang lebih baik (dan kompatibilitas yang lebih baik ke depannya) daripada std::future tidak.

Tip

Jika Anda menyertakan <pplawait.h>, maka Anda dapat menggunakan konkurensi::task sebagai jenis 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 koroutine, eksekusi sinkron hingga titik penangguhan pertama, di mana kontrol dikembalikan ke pemanggil dan bingkai panggilan keluar dari cakupan. Pada saat coroutine dilanjutkan, apa pun mungkin terjadi pada nilai sumber yang direferensikan parameter referensi. Dari perspektif koroutine, parameter referensi memiliki masa pakai yang tidak terkendali. Jadi, dalam contoh di atas, kita aman untuk mengakses nilai hingga co_await, tetapi tidak setelahnya. Jika nilai dihancurkan oleh pemanggil, mencoba mengaksesnya di dalam koroutine setelah itu mengakibatkan kerusakan memori. Kita juga tidak dapat meneruskan nilai dengan aman ke DoOtherWorkAsync jika ada risiko fungsi tersebut 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&

Melewati nilai mengharuskan argumen murah untuk dipindahkan atau disalin; dan itu biasanya terjadi untuk pointer pintar.

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