Asinkroni, dan interoperabilitas antara C++/WinRT dan C++/CX

Tip

Meskipun kami menyarankan agar Anda membaca topik ini dari awal, Anda dapat langsung melompat ke ringkasan teknik interop di bagian Ringkasan port C++/CX asinkron ke C++/WinRT .

Ini adalah topik lanjutan yang terkait dengan porting secara bertahap ke C++/WinRT dari C++/CX. Topik ini melanjutkan dari titik tempat topik Interop antara C++/WinRT dan C++/CX berakhir.

Jika ukuran atau kompleksitas basis kode Anda membuatnya perlu untuk mem-port proyek Anda secara bertahap, maka Anda akan memerlukan proses porting di mana untuk waktu C++/CX dan kode C++/WinRT ada berdampingan dalam proyek yang sama. Jika Anda memiliki kode asinkron, Anda mungkin perlu menggunakan rantai tugas Parallel Patterns Library (PPL) dan coroutine secara berdampingan dalam proyek Anda saat memigrasikan kode sumber secara bertahap. Topik ini berfokus pada teknik untuk mengoperasikan antara kode C++/CX asinkron dan kode C++/WinRT asinkron. Anda dapat menggunakan teknik ini secara individual, atau bersama-sama. Teknik-teknik ini memungkinkan Anda melakukan perubahan lokal secara bertahap dan terkontrol dalam proses mem-porting seluruh proyek Anda, tanpa membuat setiap perubahan merambat secara tak terkendali ke seluruh proyek.

Sebelum membaca topik ini, ada baiknya membaca Interop antara C++/WinRT dan C++/CX. Topik tersebut menunjukkan kepada Anda cara menyiapkan proyek Anda untuk porting bertahap. Ini juga memperkenalkan dua fungsi pembantu yang dapat Anda gunakan untuk mengonversi objek C++/CX menjadi objek C++/WinRT (dan sebaliknya). Topik tentang asinkron ini dibangun berdasarkan info tersebut, dan menggunakan fungsi pembantu tersebut.

Note

Ada beberapa batasan untuk porting secara bertahap dari C++/CX ke C++/WinRT. Jika Anda memiliki proyek komponen Windows Runtime, maka porting secara bertahap tidak dimungkinkan, dan Anda harus memindahkan proyek dalam satu pass. Dan untuk proyek XAML, pada satu waktu tipe halaman XAML Anda harus baik semuanya C++/WinRT maupun semuanya C++/CX. Untuk informasi selengkapnya, lihat topik Pindah ke C++/WinRT dari C++/CX.

Alasan satu topik khusus dikhususkan untuk interop kode asinkron

Memindahkan kode dari C++/CX ke C++/WinRT umumnya cukup mudah, dengan satu pengecualian, yaitu saat berpindah dari tugas Parallel Patterns Library (PPL) ke korutin. Modelnya berbeda. Tidak ada pemetaan alami satu-ke-satu dari tugas PPL ke koroutine, dan tidak ada cara sederhana (yang berfungsi untuk semua kasus) untuk secara mekanis memindahkan kode.

Kabar baiknya adalah bahwa konversi dari tugas ke koroutine menghasilkan penyederhanaan yang signifikan. Dan tim pengembangan secara rutin melaporkan bahwa setelah berhasil melewati kendala dalam mem-porting kode asinkron mereka, sisa pekerjaan porting sebagian besar bersifat mekanis.

Seringkali, algoritma awalnya ditulis agar sesuai dengan API sinkron. Dan kemudian itu diterjemahkan ke dalam tugas dan kelanjutan eksplisit—hasilnya sering menjadi penghalang yang tidak disengaja dari logika yang mendasar. Misalnya, perulangan menjadi rekursi; cabang if-else berubah menjadi pohon berlapis (rantai) tugas; variabel bersama menjadi shared_ptr. Untuk mendekonstruksi struktur kode sumber PPL yang sering tidak wajar, kami sarankan Anda terlebih dahulu mundur dan memahami niat kode asli (yaitu, temukan versi sinkron asli). Lalu sisipkan co_await (menunggu secara kooperatif) di tempat yang sesuai.

Untuk alasan itu, jika Anda memiliki versi C# (bukan C++/CX) dari kode asinkron untuk memulai port Anda, maka itu dapat memberi Anda waktu yang lebih mudah, dan port yang lebih bersih. Kode C# menggunakan await. Jadi kode C# pada dasarnya sudah mengikuti filosofi dimulai dengan versi sinkron dan kemudian memasukkan await ke tempat yang sesuai.

Jika Anda tidak memiliki versi C# dari proyek Anda, maka Anda dapat menggunakan teknik yang dijelaskan dalam topik ini. Dan setelah Anda melakukan port ke C++/WinRT, struktur kode asinkron Anda kemudian akan lebih mudah di-port ke C#, jika Anda mau.

Beberapa latar belakang dalam pemrograman asinkron

Agar kita memiliki kerangka acuan bersama untuk konsep dan terminologi pemrograman asinkron, mari kita bahas secara singkat pemrograman asinkron Windows Runtime secara umum, serta bagaimana masing-masing dari dua proyeksi bahasa C++ tersebut, dengan caranya yang berbeda, dibangun di atasnya.

Proyek Anda memiliki metode yang bekerja secara asinkron, dan ada dua jenis utama.

  • Adalah umum untuk ingin menunggu penyelesaian pekerjaan asinkron sebelum Anda melakukan sesuatu yang lain. Metode yang mengembalikan objek operasi asinkron adalah metode yang dapat Anda tunggu.
  • Tetapi terkadang Anda tidak ingin atau perlu menunggu penyelesaian pekerjaan yang dilakukan secara asinkron. Dalam kasus tersebut, akan lebih efisien jika metode asinkron tidak mengembalikan objek operasi asinkron. Metode asinkron seperti itu—metode yang tidak perlu Anda tunggu—dikenal sebagai metode fire-and-forget.

objek asinkron Windows Runtime (IAsyncXxx)

Namespace Windows Runtime Windows::Foundation berisi empat jenis objek operasi asinkron.

Dalam topik ini, ketika kita menggunakan singkatan yang nyaman dari IAsyncXxx, kita mengacu pada jenis ini secara kolektif; atau kita berbicara tentang salah satu dari empat jenis tanpa perlu menentukan yang mana.

C++/CX asinkron

Kode C++/CX asinkron menggunakan tugas Parallel Patterns Library (PPL ). Tugas PPL diwakili oleh kelas konkurensi::tugas .

Biasanya, metode asinkron C++/CX merangkai tugas-tugas PPL dengan fungsi lambda melalui concurrency::create_task dan concurrency::task::then. Setiap fungsi lambda mengembalikan tugas yang, ketika selesai, menghasilkan nilai yang kemudian diteruskan ke lambda kelanjutan tugas.

Atau, alih-alih memanggil create_task untuk membuat tugas, metode C++/CX asinkron dapat memanggil konkurensi::create_async untuk membuat IAsyncXxx^.

Jadi jenis pengembalian metode C++/CX asinkron dapat menjadi tugas PPL, atau IAsyncXxx^.

Dalam kedua kasus, metode itu sendiri menggunakan return kata kunci untuk mengembalikan objek asinkron yang, ketika selesai, menghasilkan nilai yang sebenarnya diinginkan pemanggil (mungkin file, array byte, atau Boolean).

Note

Jika metode C++/CX asinkron mengembalikan IAsyncXxx^, maka TResult (jika ada) terbatas pada jenis Windows Runtime. Nilai Boolean, misalnya, adalah tipe Windows Runtime; tetapi tipe hasil proyeksi C++/CX (misalnya, Platform::Array<byte>^) bukan.

C++/WinRT asinkron

C++/WinRT mengintegrasikan koroutin C++ ke dalam model pemrograman. Coroutines dan pernyataan co_await memberikan cara alami untuk menunggu hasil secara kooperatif.

Masing-masing jenis IAsyncXxx direpresentasikan sebagai jenis yang sesuai dalam namespace C++/WinRT winrt::Windows::Foundation. Mari kita sebut sebagai winrt::IAsyncXxx (dibandingkan dengan IAsyncXxx^ dari C++/CX).

Jenis pengembalian coroutine C++/WinRT adalah winrt::IAsyncXxx, atau winrt::fire_and_forget. Dan alih-alih menggunakan kata kunci return untuk mengembalikan objek asinkron, korutin menggunakan kata kunci co_return untuk mengembalikan secara kooperatif nilai yang benar-benar diinginkan oleh pemanggil (mungkin berkas, larik byte, atau Boolean).

Jika suatu metode memuat setidaknya satu pernyataan co_await (atau setidaknya satu co_return atau co_yield), maka metode tersebut merupakan korutin karena itu.

Untuk informasi selengkapnya, dan contoh kode, lihat Operasi konkurensi dan asinkron dengan C++/WinRT.

Sampel gim Direct3D (Simple3DGameDX)

Topik ini berisi panduan beberapa teknik pemrograman tertentu yang menggambarkan cara memindahkan kode asinkron secara bertahap. Untuk berfungsi sebagai studi kasus, kita akan menggunakan versi C++/CX dari sampel game Direct3D (yang disebut Simple3DGameDX). Kami akan menunjukkan beberapa contoh bagaimana Anda dapat mengambil kode sumber C++/CX asli dalam proyek tersebut dan secara bertahap memindahkan kode asinkronnya ke C++/WinRT.

  • Unduh ZIP dari tautan di atas, dan unzip.
  • Buka proyek C++/CX (ada di folder bernama cpp) di Visual Studio.
  • Anda kemudian perlu menambahkan dukungan C++/WinRT ke proyek. Langkah-langkah yang Anda ikuti untuk melakukannya dijelaskan dalam Mengambil proyek C++/CX dan menambahkan dukungan C++/WinRT. Di bagian itu, langkah tentang menambahkan interop_helpers.h file header ke proyek Anda sangat penting karena kita akan bergantung pada fungsi pembantu tersebut dalam topik ini.
  • Terakhir, tambahkan #include <pplawait.h> ke pch.h. Hal itu memberi Anda dukungan coroutine untuk PPL (penjelasan lebih lanjut tentang dukungan tersebut ada di bagian berikut).

Jangan lakukan build dulu, kalau tidak Anda akan mendapatkan error karena byte ambigu. Berikut cara mengatasinya.

  • Buka BasicLoader.cpp, dan jadikan using namespace std; sebagai komentar.
  • Dalam file kode sumber yang sama, Anda kemudian harus memenuhi syarat shared_ptr sebagai std::shared_ptr. Anda dapat melakukannya dengan pencarian dan penggantian dalam file tersebut.
  • Kemudian kualifikasi vektor sebagai std::vector, dan string sebagai std::string.

Proyek sekarang dapat dikompilasi lagi, memiliki dukungan C++/WinRT, dan menyertakan fungsi pembantu interoperabilitas from_cx dan to_cx.

Anda sekarang memiliki proyek Simple3DGameDX yang siap diikuti bersama dengan panduan kode dalam topik ini.

Gambaran umum porting C++/CX asinkron ke C++/WinRT

Singkatnya, saat kita melakukan porting, kita akan mengubah rangkaian tugas PPL menjadi panggilan ke co_await. Kita akan mengubah nilai pengembalian metode dari tugas PPL menjadi objek C++/WinRT winrt::IAsyncXxx . Dan kita juga akan mengubah IAsyncXxx^ menjadi C++/WinRT winrt::IAsyncXxx.

Anda mungkin ingat bahwa coroutine adalah metode apa pun yang memanggil co_xxx. Coroutine C++/WinRT menggunakan co_return untuk mengembalikan nilainya secara kooperatif. Berkat dukungan coroutine untuk PPL (atas kontribusi pplawait.h), Anda juga dapat menggunakan co_return untuk mengembalikan task PPL dari sebuah coroutine. Dan Anda juga dapat co_await baik tugas maupun IAsyncXxx. Tetapi Anda tidak dapat menggunakan co_return untuk mengembalikan IAsyncXxx^. Tabel di bawah ini menjelaskan dukungan untuk interoperabilitas antara berbagai teknik asinkron yang ditunjukkan dengan pplawait.h pada gambar.

Metode Dapatkah Anda co_await itu? Dapatkah Anda co_return dari itu?
Metode mengembalikan task<void> Yes Yes
Metode mengembalikan task<T> No Yes
Metode mengembalikan IAsyncXxx^ Yes No. Tetapi Anda membungkus create_async di sekitar tugas yang menggunakan co_return.
Metode mengembalikan winrt::IAsyncXxx Yes Yes

Gunakan tabel berikutnya ini untuk melompat langsung ke bagian dalam topik ini yang menjelaskan teknik interop yang menarik, atau hanya melanjutkan membaca dari sini.

Teknik interop asinkron Bagian dalam topik ini
Gunakan co_await untuk meng-await metode task<void> dari dalam metode fire-and-forget, atau di dalam konstruktor. Menunggu task<void> dalam metode fire-and-forget
Gunakan co_await untuk await metode task<void> dari dalam metode task<void>. Gunakan await task<void> dalam metode task<void>
Gunakan co_await untuk await metode task<void> di dalam metode task<T>. Menunggu tugas<batal> dalam metode tugas<T>
Gunakan co_await untuk menunggu metode IAsyncXxx^. Menantikan IAsyncXxx^ dalam metode task tanpa mengubah bagian proyek lainnya
Gunakan co_return di dalam metode task<void>. Menunggu task<void> di dalam metode task<void>
Gunakan co_return dalam metode tugas< T>. Menunggu IAsyncXxx^ dalam metode tugas , membiarkan sisa proyek tidak berubah
Bungkus create_async di sekitar tugas yang menggunakan co_return. Membungkus create_async di sekitar tugas yang menggunakan co_return
Konkurensi port::tunggu. Porting concurrency::wait ke co_await winrt::resume_after
Gunakan winrt::IAsyncXxx alih-alih task<void>. Porting tipe pengembalian task<void> ke winrt::IAsyncXxx
Mengubah winrt::IAsyncXxx<T> (T adalah tipe primitif) menjadi task<T>. Mengonversi winrt::IAsyncXxx<T> (T primitif) ke tugas<T>
Mengonversi winrt::IAsyncXxx<T> (T adalah jenis Windows Runtime) menjadi tugas<T^>. Mengonversi winrt::IAsyncXxx<T> (T adalah jenis Windows Runtime) ke tugas<T^>

Dan berikut adalah contoh kode singkat yang mengilustrasikan beberapa dukungan.

#include <ppltasks.h>
#include <pplawait.h>
#include <winrt/Windows.Foundation.h>

concurrency::task<bool> TaskAsync()
{
    co_return true;
}

Windows::Foundation::IAsyncOperation<bool>^ IAsyncXxxCppCXAsync()
{
    // co_return true; // Error! Can't do that. But you can do
    // the following.
    return concurrency::create_async([=]() -> concurrency::task<bool> {
        co_return true;
        });
}

winrt::Windows::Foundation::IAsyncOperation<bool> IAsyncXxxCppWinRTAsync()
{
    co_return true;
}

concurrency::task<bool> CppCXAsync()
{
    bool b1 = co_await TaskAsync();
    bool b2 = co_await IAsyncXxxCppCXAsync();
    co_return co_await IAsyncXxxCppWinRTAsync();
}

winrt::fire_and_forget CppWinRTAsync()
{
    bool b1 = co_await TaskAsync();
    bool b2 = co_await IAsyncXxxCppCXAsync();
    bool b3 = co_await IAsyncXxxCppWinRTAsync();
}

Important

Bahkan dengan opsi interop yang baik ini, melakukan porting secara bertahap bergantung pada pemilihan perubahan yang dapat kita lakukan secara presisi tanpa memengaruhi bagian lain dari proyek. Kami ingin menghindari menarik satu ujung yang terlepas secara sembarangan, yang pada akhirnya dapat mengurai seluruh struktur proyek. Untuk itu, kita harus melakukan hal-hal dalam urutan tertentu. Selanjutnya kita akan melihat lebih dekat beberapa contoh pembuatan jenis perubahan porting/interop terkait asinkron ini.

Gunakan await pada metode task<void> tanpa mengubah bagian lain dari proyek

Metode yang mengembalikan task<void> menjalankan operasi secara asinkron dan mengembalikan objek operasi asinkron, tetapi pada akhirnya tidak menghasilkan nilai. Kita bisa co_await metode seperti itu.

Jadi tempat yang baik untuk mulai porting kode asinkron secara bertahap adalah menemukan tempat di mana Anda memanggil metode tersebut. Tempat-tempat tersebut akan melibatkan pembuatan dan/atau mengembalikan tugas. Mereka juga mungkin melibatkan jenis rangkaian tugas di mana tidak ada nilai yang diteruskan dari setiap tugas ke kelanjutannya. Di tempat-tempat seperti itu, Anda hanya dapat mengganti kode asinkron dengan co_await pernyataan, seperti yang akan kita lihat.

Note

Seiring berjalannya topik ini, Anda akan melihat manfaat dari strategi ini. Setelah metode void< tugas> tertentu dipanggil secara eksklusif melalui co_await, Anda kemudian bebas untuk memindahkan metode tersebut ke C++/WinRT, dan memintanya mengembalikan winrt::IAsyncXxx.

Mari kita temukan beberapa contoh. Buka proyek Simple3DGameDX (lihat Sampel game Direct3D).

Important

Dalam contoh-contoh berikut, saat Anda melihat implementasi metode diubah, perlu diingat bahwa kita tidak perlu mengubah pemanggil dari metode yang kita ubah. Perubahan ini bersifat lokal dan tidak merambat ke seluruh proyek.

Gunakan await pada task<void> dalam metode fire-and-forget

Mari kita mulai dengan menanti task<void> di dalam metode fire-and-forget, karena itulah kasus yang paling sederhana. Ini adalah metode yang bekerja secara asinkron, tetapi pemanggil metode tidak menunggu pekerjaan tersebut selesai. Anda cukup memanggil metode tersebut lalu langsung melupakannya, meskipun eksekusinya selesai secara asinkron.

Periksa akar dari grafik dependensi proyek Anda untuk menemukan metode void yang berisi create_task dan/atau rantai tugas yang hanya memanggil metode task<void>.

Di Simple3DGameDX, Anda akan menemukan kode seperti itu dalam implementasi metode GameMain::Update. Ini ada dalam file GameMain.cppkode sumber .

GameMain::Update

Berikut adalah ekstrak dari versi C++/CX dari metode , memperlihatkan dua bagian metode yang selesai secara asinkron.

void GameMain::Update()
{
    ...
    case UpdateEngineState::WaitingForPress:
        ...
        m_game->LoadLevelAsync().then([this]()
        {
            m_game->FinalizeLoadLevel();
            m_updateState = UpdateEngineState::ResourcesLoaded;
        }, task_continuation_context::use_current());
        ...
    case UpdateEngineState::Dynamics:
        ...
        m_game->LoadLevelAsync().then([this]()
        {
            m_game->FinalizeLoadLevel();
            m_updateState = UpdateEngineState::ResourcesLoaded;
        }, task_continuation_context::use_current());
        ...
    ...
}

Anda dapat melihat pemanggilan metode Simple3DGame::LoadLevelAsync (yang mengembalikan PPL task<void>). Setelah itu ada continuation yang melakukan beberapa tugas sinkron. LoadLevelAsync bersifat asinkron, tetapi tidak mengembalikan nilai. Jadi tidak ada nilai yang diteruskan dari tugas ke kelanjutan.

Kita dapat membuat perubahan yang sama pada kode di dua tempat ini. Kodenya dijelaskan setelah cuplikan kode di bawah ini. Kita bisa berdiskusi di sini tentang cara yang aman untuk mengakses pointer ini dalam koroutine anggota kelas. Tetapi mari kita tangguhkan itu untuk bagian selanjutnya (Diskusi yang ditangguhkan tentang co_await dan penunjuk ini)—untuk saat ini, kode ini berfungsi.

winrt::fire_and_forget GameMain::Update()
{
    ...
    case UpdateEngineState::WaitingForPress:
        ...
        co_await m_game->LoadLevelAsync();
        m_game->FinalizeLoadLevel();
        m_updateState = UpdateEngineState::ResourcesLoaded;
        ...
    case UpdateEngineState::Dynamics:
        ...
        co_await m_game->LoadLevelAsync();
        m_game->FinalizeLoadLevel();
        m_updateState = UpdateEngineState::ResourcesLoaded;
        ...
    ...
}

Seperti yang Anda lihat, karena LoadLevelAsync mengembalikan tugas, kita bisa co_await melakukannya. Dan kita tidak memerlukan kelanjutan eksplisit—kode yang mengikuti co_await eksekusi hanya ketika LoadLevelAsync selesai.

Menambahkan co_await mengubah metode tersebut menjadi coroutine, sehingga kami tidak dapat membiarkannya tetap mengembalikan void. Ini adalah metode fire-and-forget, jadi kami mengubahnya agar mengembalikan winrt::fire_and_forget.

Anda juga perlu mengedit GameMain.h. Ubah jenis pengembalian GameMain::Perbarui dari void ke winrt::fire_and_forget dalam deklarasi di sana juga.

Anda dapat melakukan perubahan ini pada salinan proyek Anda, dan gim tersebut tetap dapat dikompilasi dan dijalankan seperti biasa. Kode sumber masih pada dasarnya C++/CX, tetapi sekarang menggunakan pola yang sama dengan C++/WinRT, sehingga telah memindahkan kita sedikit lebih dekat untuk dapat memindahkan sisa kode secara mekanis.

GameMain::ResetGame

GameMain::ResetGame adalah metode fire-and-forget lainnya; ini juga memanggil LoadLevelAsync. Dengan begitu, Anda bisa melakukan perubahan kode yang sama di sana jika ingin berlatih.

GameMain::OnDeviceRestored

Hal-hal menjadi sedikit lebih menarik di GameMain::OnDeviceRestored karena bersarangnya kode asinkron yang lebih dalam, termasuk tugas no-op. Berikut adalah kerangka bagian asinkron dari metode (dengan kode sinkron yang kurang menarik yang diwakili oleh elipsis).

void GameMain::OnDeviceRestored()
{
    ...
    create_task([this]()
    {
        return m_renderer->CreateGameDeviceResourcesAsync(m_game);
    }).then([this]()
    {
        ...
        if (m_updateState == UpdateEngineState::WaitingForResources)
        {
            ...
            return m_game->LoadLevelAsync().then([this]()
            {
                ...
            }, task_continuation_context::use_current());
        }
        else
        {
            return create_task([]()
            {
                // Return a no-op task.
            });
        }
    }, task_continuation_context::use_current()).then([this]()
    {
        ...
    }, task_continuation_context::use_current());
}

Pertama, ubah jenis pengembalian GameMain::OnDeviceRestored dari void ke winrt::fire_and_forget di GameMain.h dan .cpp. Anda juga harus membuka DeviceResources.h dan membuat perubahan yang sama pada jenis pengembalian IDeviceNotify::OnDeviceRestored.

Untuk mem-porting kode asinkron, hapus semua pemanggilan create_task dan then beserta tanda kurung kurawalnya, lalu sederhanakan metode tersebut menjadi rangkaian pernyataan linear.

Ubah apa pun return yang mengembalikan tugas menjadi co_await. Anda akan memiliki satu return yang tidak mengembalikan apa pun, jadi hapus saja. Setelah Anda selesai, tugas no-op akan hilang, dan garis besar bagian-bagian asinkron dari metode tersebut akan terlihat seperti ini. Sekali lagi, kode sinkron yang kurang menarik dihilangkan.

winrt::fire_and_forget GameMain::OnDeviceRestored()
{
    ...
    co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);
    ...
    if (m_updateState == UpdateEngineState::WaitingForResources)
    {
        co_await m_game->LoadLevelAsync();
        ...
    }
    ...
}

Seperti yang Anda lihat, bentuk struktur asinkron ini secara signifikan lebih sederhana, dan lebih mudah dibaca.

GameMain::GameMain

Konstruktor GameMain::GameMain melakukan pekerjaan secara asinkron, dan tidak ada bagian dari proyek yang menunggu pekerjaan tersebut selesai. Sekali lagi, daftar ini menguraikan bagian asinkron.

GameMain::GameMain(...) : ...
{
    ...
    create_task([this]()
    {
        ...
        return m_renderer->CreateGameDeviceResourcesAsync(m_game);
    }).then([this]()
    {
        ...
        if (m_updateState == UpdateEngineState::WaitingForResources)
        {
            return m_game->LoadLevelAsync().then([this]()
            {
                ...
            }, task_continuation_context::use_current());
        }
        else
        {
            return create_task([]()
            {
                // Return a no-op task.
            });
        }
    }, task_continuation_context::use_current()).then([this]()
    {
        ....
    }, task_continuation_context::use_current());
}

Tetapi konstruktor tidak dapat mengembalikan winrt::fire_and_forget, jadi kita akan memindahkan kode asinkron ke dalam metode fire-and-forget GameMain::ConstructInBackground baru, meratakan kode ke dalam co_await pernyataan, dan memanggil metode baru dari konstruktor. Berikut hasilnya.

GameMain::GameMain(...) : ...
{
    ...
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    ...
    co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);
    ...
    if (m_updateState == UpdateEngineState::WaitingForResources)
    {
        ...
        co_await m_game->LoadLevelAsync();
        ...
    }
    ...
}

Sekarang semua metode fire-and-forget—sebenarnya, semua kode asinkron—di GameMain telah diubah menjadi coroutine. Jika Anda berkenan, mungkin Anda dapat mencari metode fire-and-forget di kelas lain dan melakukan perubahan serupa.

Pembahasan yang ditunda tentang co_await dan penunjuk this

Ketika kami membuat perubahan pada GameMain::Update, saya menunda diskusi tentang pointer ini . Mari kita diskusikan di sini.

Ini berlaku untuk semua metode yang sudah kita ubah sejauh ini; dan hal ini juga berlaku untuk semua coroutine, bukan hanya yang bersifat fire-and-forget. Menambahkan co_await ke dalam metode akan menghasilkan titik suspensi. Dan karena itu, kita harus berhati-hati dengan pointer this, yang tentu saja kita gunakan setelah titik suspensi setiap kali kita mengakses anggota kelas.

Singkatnya adalah bahwa solusinya adalah memanggil implements::get_strong. Tetapi untuk diskusi lengkap tentang masalah dan solusinya, lihat Mengakses pointer ini dengan aman dalam koroutin anggota kelas.

Anda dapat memanggil implements::get_strong hanya di kelas yang berasal dari winrt::implements.

Derive GameMain dari winrt::implements

Perubahan pertama yang perlu kita lakukan adalah di GameMain.h.

class GameMain :
    public DX::IDeviceNotify

GameMain akan terus mengimplementasikan DX::IDeviceNotify, tetapi kami akan mengubahnya menjadi berasal dari winrt::implements.

class GameMain : 
    public winrt::implements<GameMain, winrt::Windows::Foundation::IInspectable>,
    DX::IDeviceNotify

Selanjutnya, dalam App.cpp, Anda akan menemukan metode ini.

void App::Load(Platform::String^)
{
    if (!m_main)
    {
        m_main = std::unique_ptr<GameMain>(new GameMain(m_deviceResources));
    }
}

Tetapi sekarang gameMain berasal dari winrt::implements, kita perlu membangunnya dengan cara yang berbeda. Dalam hal ini, kita akan menggunakan templat fungsi winrt::make_self . Untuk informasi selengkapnya, lihat Membuat instance dan mengembalikan tipe dan antarmuka implementasi.

Ganti baris kode tersebut dengan ini.

    ...
    m_main = winrt::make_self<GameMain>(m_deviceResources);
    ...

Untuk menuntaskan perubahan itu, kita juga perlu mengubah tipe m_main. Di App.h, Anda akan menemukan kode ini.

ref class App sealed :
    public Windows::ApplicationModel::Core::IFrameworkView
{
    ...
private:
    ...
    std::unique_ptr<GameMain> m_main;
};

Ubah deklarasi m_main ke ini.

    ...
    winrt::com_ptr<GameMain> m_main;
    ...

Kita sekarang dapat memanggil implements::get_strong

Untuk GameMain::Update, dan untuk metode lain mana pun yang kami tambahi co_await, berikut cara Anda dapat memanggil get_strong di awal coroutine untuk memastikan bahwa referensi kuat tetap bertahan hingga coroutine selesai.

winrt::fire_and_forget GameMain::Update()
{
    auto strong_this{ get_strong() }; // Keep *this* alive.
    ...
        co_await ...
    ...
}

Menunggu task<void> dalam metode task<void>

Kasus paling sederhana berikutnya adalah menunggu task<void> dalam metode yang mengembalikan task<void>. Itu karena kita bisa co_await membatalkan tugas<>, dan kita bisa co_return dari satu.

Anda akan menemukan contoh yang sangat sederhana dalam implementasi metode Simple3DGame::LoadLevelAsync. Ini ada dalam file Simple3DGame.cppkode sumber .

task<void> Simple3DGame::LoadLevelAsync()
{
    m_level[m_currentLevel]->Initialize(m_objects);
    m_levelDuration = m_level[m_currentLevel]->TimeLimit() + m_levelBonusTime;
    return m_renderer->LoadLevelResourcesAsync();
}

Hanya ada beberapa kode sinkron, diikuti dengan mengembalikan tugas yang dibuat oleh GameRenderer::LoadLevelResourcesAsync.

Alih-alih mengembalikan tugas itu, kami co_await , dan kemudian co_return yang dihasilkan void.

task<void> Simple3DGame::LoadLevelAsync()
{
    m_level[m_currentLevel]->Initialize(m_objects);
    m_levelDuration = m_level[m_currentLevel]->TimeLimit() + m_levelBonusTime;
    co_return co_await m_renderer->LoadLevelResourcesAsync();
}

Itu tidak terlihat seperti perubahan besar. Tetapi sekarang karena kita memanggil GameRenderer::LoadLevelResourcesAsync melalui co_await, kita bebas mem-portingnya agar mengembalikan winrt::IAsyncXxx alih-alih task. Kita akan melakukannya nanti di bagian Mem-port tipe pengembalian task<void> ke winrt::IAsyncXxx.

Menunggu tugas<batal> dalam metode tugas<T>

Meskipun tidak ada contoh yang cocok untuk ditemukan di Simple3DGameDX, kita dapat mengambil contoh hipotetis hanya untuk menunjukkan polanya.

Baris pertama dalam contoh kode di bawah ini menunjukkan co_awaittask<void> sederhana. Kemudian, untuk memenuhi tipe pengembalian task<T>, kita perlu mengembalikan StorageFile^ secara asinkron. Untuk melakukannya, kami co_await API Windows Runtime, dan co_return berkas yang dihasilkan.

task<StorageFile^> Simple3DGame::LoadLevelAndRetrieveFileAsync(
    StorageFolder^ location,
    Platform::String^ filename)
{
    co_await m_renderer->LoadLevelResourcesAsync();
    co_return co_await location->GetFileAsync(filename);
}

Kita bahkan dapat memindahkan lebih banyak metode ke C++/WinRT seperti ini.

winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::StorageFile>
Simple3DGame::LoadLevelAndRetrieveFileAsync(
    StorageFolder location,
    std::wstring filename)
{
    co_await m_renderer->LoadLevelResourcesAsync();
    co_return co_await location.GetFileAsync(filename);
}

Anggota data m_renderer masih C++/CX dalam contoh tersebut.

Menunggu IAsyncXxx^ dalam metode tugas , membiarkan sisa proyek tidak berubah

Kita telah melihat bagaimana Anda dapat co_awaittask<void>. Anda juga dapat co_await metode yang mengembalikan IAsyncXxx, baik metode di proyek Anda maupun API Windows asinkron (misalnya, StorageFolder.GetFileAsync, yang telah kita tunggu secara kooperatif pada bagian sebelumnya).

Untuk contoh di mana kita dapat membuat perubahan kode semacam ini, mari kita lihat BasicReaderWriter::ReadDataAsync (Anda akan menemukannya diimplementasikan di BasicReaderWriter.cpp).

Berikut adalah versi C++/CX asli.

task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename
    )
{
    return task<StorageFile^>(m_location->GetFileAsync(filename)).then([=](StorageFile^ file)
    {
        return FileIO::ReadBufferAsync(file);
    }).then([=](IBuffer^ buffer)
    {
        auto fileData = ref new Platform::Array<byte>(buffer->Length);
        DataReader::FromBuffer(buffer)->ReadBytes(fileData);
        return fileData;
    });
}

Daftar kode di bawah ini menunjukkan bahwa kita dapat co_await Windows API yang mengembalikan IAsyncXxx^. Tidak hanya itu, kita juga dapat co_return mendapatkan nilai yang BasicReaderWriter::ReadDataAsync kembalikan secara asinkron (dalam hal ini, larik byte). Langkah pertama ini menunjukkan cara membuat perubahan tersebut saja; kita benar-benar akan memindahkan kode C++/CX ke C++/WinRT di bagian berikutnya.

task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename
)
{
    StorageFile^ file = co_await m_location->GetFileAsync(filename);
    IBuffer^ buffer = co_await FileIO::ReadBufferAsync(file);
    auto fileData = ref new Platform::Array<byte>(buffer->Length);
    DataReader::FromBuffer(buffer)->ReadBytes(fileData);
    co_return fileData;
}

Sekali lagi, kita tidak perlu mengubah pemanggil dari metode yang kita ubah, karena kita tidak mengubah tipe nilai balik.

Port ReadDataAsync (sebagian besar) ke C++/WinRT, membiarkan sisa proyek tidak berubah

Kita dapat melangkah lebih jauh dan memindahkan metode hampir sepenuhnya ke C++/WinRT tanpa perlu mengubah bagian lain dari proyek.

Satu-satunya dependensi yang dimiliki metode ini pada sisa proyek adalah anggota data BasicReaderWriter::m_location , yang merupakan C++/CX StorageFolder^. Untuk membiarkan anggota data tersebut tetap tidak berubah, serta membiarkan tipe parameter dan tipe nilai balik tetap tidak berubah, kita hanya perlu melakukan dua konversi—satu di awal metode dan satu di akhir. Untuk itu, kita dapat menggunakan fungsi pembantu interop from_cx dan to_cx .

Berikut adalah bagaimana BasicReaderWriter::ReadDataAsync terlihat setelah porting implementasinya sebagian besar ke C++/WinRT. Ini adalah contoh yang baik tentang melakukan porting secara bertahap. Dan metode ini berada pada tahap di mana kita dapat menjauh dari anggapan itu sebagai metode C++/CX yang menggunakan beberapa teknik C++/WinRT, dan melihatnya sebagai metode C++/WinRT yang saling menginteroperaksi dengan C++/CX.

#include <winrt/Windows.Storage.h>
#include <winrt/Windows.Storage.Streams.h>
#include <robuffer.h>
...
task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename)
{
    auto location_from_cx = from_cx<winrt::Windows::Storage::StorageFolder>(m_location);

    auto file = co_await location_from_cx.GetFileAsync(filename->Data());
    auto buffer = co_await winrt::Windows::Storage::FileIO::ReadBufferAsync(file);
    byte* bytes;
    auto byteAccess = buffer.as<Windows::Storage::Streams::IBufferByteAccess>();
    winrt::check_hresult(byteAccess->Buffer(&bytes));

    co_return ref new Platform::Array<byte>(bytes, buffer.Length());
}

Note

Dalam ReadDataAsync di atas, kami membangun dan mengembalikan array C++/CX baru. Dan tentu saja kita melakukan itu untuk memenuhi jenis pengembalian metode (sehingga kita tidak perlu mengubah sisa proyek).

Anda mungkin menemukan contoh lain dalam proyek Anda sendiri di mana, setelah porting, Anda mencapai akhir metode dan yang Anda miliki adalah objek C++/WinRT. Untuk co_return itu, cukup panggil to_cx untuk mengonversinya. Ada info selengkapnya tentang itu, dan contohnya, bagian berikutnya.

Mengubah winrt::IAsyncXxx<T> menjadi task<T>

Bagian ini berkaitan dengan situasi di mana Anda telah mentransfer metode asinkron ke C++/WinRT (sehingga mengembalikan winrt::IAsyncXxx<T>), tetapi Anda masih memiliki kode C++/CX yang memanggil metode tersebut seolah-olah masih mengembalikan tugas.

  • Satu kasus adalah di mana T primitif, yang tidak membutuhkan konversi.
  • Kasus lain adalah di mana T adalah jenis Windows Runtime, dalam hal ini Anda harus mengonversinya menjadi T^.

Mengubah winrt::IAsyncXxx<T> (T adalah tipe primitif) menjadi task<T>

Pola di bagian ini berlaku saat Anda secara asinkron mengembalikan nilai primitif (kita akan menggunakan nilai Boolean untuk mengilustrasikan). Pertimbangkan contoh di mana metode yang telah Anda port ke C++/WinRT memiliki tanda tangan ini.

winrt::Windows::Foundation::IAsyncOperation<bool>
MyClass::GetBoolMemberFunctionAsync()
{
    bool value = ...
    co_return value;
}

Anda dapat mengonversi panggilan ke metode tersebut menjadi tugas seperti ini.

task<bool> MyClass::RetrieveBoolTask()
{
    co_return co_await GetBoolMemberFunctionAsync();
}

Atau seperti ini.

task<bool> MyClass::RetrieveBoolTask()
{
    return concurrency::create_task(
        [this]() -> concurrency::task<bool> {
            auto result = co_await GetBoolMemberFunctionAsync();
            co_return result;
        });
}

Perhatikan bahwa tipe pengembalian task dari fungsi lambda dinyatakan secara eksplisit, karena kompiler tidak dapat menyimpulkannya.

Kita juga dapat memanggil metode dari dalam rantai tugas arbitrer seperti ini. Sekali lagi, dengan tipe nilai balik lambda yang eksplisit.

...
.then([this]() -> concurrency::task<bool> {
    co_return co_await GetBoolMemberFunctionAsync();
}).then([this](bool result) {
    ...
});
...

Mengonversi winrt::IAsyncXxx<T> (T adalah jenis Windows Runtime) ke tugas<T^>

Pola di bagian ini berlaku saat Anda mengembalikan nilai Windows Runtime secara asinkron (kita akan menggunakan nilai StorageFile untuk mengilustrasikan). Pertimbangkan contoh di mana metode yang telah Anda port ke C++/WinRT memiliki tanda tangan ini.

winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::StorageFile>
MyClass::GetStorageFileMemberFunctionAsync()
{
    co_return co_await winrt::Windows::Storage::StorageFile::GetFileFromPathAsync
    (L"MyFile.txt");
}

Daftar berikutnya ini memperlihatkan cara mengonversi panggilan ke metode tersebut menjadi tugas. Perhatikan bahwa kita perlu memanggil fungsi pembantu interop to_cx untuk mengonversi objek C++/WinRT yang dikembalikan menjadi objek handle C++/CX (juga dikenal sebagai objek hat).

task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
    winrt::Windows::Storage::StorageFile storageFile =
        co_await GetStorageFileMemberFunctionAsync();
    co_return to_cx<Windows::Storage::StorageFile>(storageFile);
}

Berikut adalah versi yang lebih singkat dari itu.

task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
    co_return to_cx<Windows::Storage::StorageFile>(GetStorageFileMemberFunctionAsync());
}

Dan Anda bahkan dapat memilih untuk membungkus pola tersebut menjadi templat fungsi yang bisa digunakan kembali, dan return mengembalikannya seperti Anda biasanya mengembalikan task.

template<typename ResultTypeCX, typename Awaitable>
concurrency::task<ResultTypeCX^> to_task(Awaitable awaitable)
{
    co_return to_cx<ResultTypeCX>(co_await awaitable);
}

task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
    return to_task<Windows::Storage::StorageFile>(GetStorageFileMemberFunctionAsync());
}

Jika Anda menyukai ide itu, Anda mungkin ingin menambahkan to_task ke interop_helpers.h.

Membungkus create_async di sekitar tugas yang menggunakan co_return

Anda tidak dapat co_returnIAsyncXxx^ secara langsung, tetapi Anda dapat melakukan sesuatu yang serupa. Jika Anda memiliki tugas yang secara kooperatif mengembalikan nilai, maka Anda dapat membungkusnya di dalam panggilan ke konkurensi::create_async.

Berikut adalah contoh hipotetis, karena tidak ada contoh yang dapat kita angkat dari Simple3DGameDX.

Windows::Foundation::IAsyncOperation<bool>^ MyClass::RetrieveBoolAsync()
{
    return concurrency::create_async(
        [this]() -> concurrency::task<bool> {
            bool result = co_await GetBoolMemberFunctionAsync();
            co_return result;
        });
}

Seperti yang Anda lihat, Anda dapat memperoleh nilai balik dari metode apa pun yang dapat Anda co_await.

Porting concurrency::wait ke co_await winrt::resume_after

Ada beberapa tempat di mana Simple3DGameDX menggunakan konkurensi::tunggu untuk menjeda utas untuk waktu yang singkat. Berikut ini contohnya.

// GameConstants.h
namespace GameConstants
{
    ...
    static const int InitialLoadingDelay = 2000;
    ...
}

// GameRenderer.cpp
task<void> GameRenderer::CreateGameDeviceResourcesAsync(_In_ Simple3DGame^ game)
{
    std::vector<task<void>> tasks;
    ...
    tasks.push_back(create_task([]()
    {
        wait(GameConstants::InitialLoadingDelay);
    }));
    ...
}

Versi C++/WinRT dari concurrency::wait adalah struktur winrt::resume_after. Kita dapat co_await struct itu di dalam tugas PPL. Berikut adalah contoh kode.

// GameConstants.h
namespace GameConstants
{
    using namespace std::literals::chrono_literals;
    ...
    static const auto InitialLoadingDelay = 2000ms;
    ...
}

// GameRenderer.cpp
task<void> GameRenderer::CreateGameDeviceResourcesAsync(_In_ Simple3DGame^ game)
{
    std::vector<task<void>> tasks;
    ...
    tasks.push_back(create_task([]() -> task<void>
    {
        co_await winrt::resume_after(GameConstants::InitialLoadingDelay);
    }));
    ...
}

Perhatikan dua perubahan lain yang harus kami lakukan. Kami mengubah jenis GameConstants::InitialLoadingDelay menjadi std::chrono::d uration, dan kami membuat jenis pengembalian fungsi lambda secara eksplisit, karena pengkompilasi tidak lagi dapat menyimpulkannya.

Ubah tipe pengembalian task<void> menjadi winrt::IAsyncXxx

Simple3DGame::LoadLevelAsync

Pada tahap ini, dalam pekerjaan kami pada Simple3DGameDX, semua bagian dalam proyek yang memanggil Simple3DGame::LoadLevelAsync menggunakan co_await untuk memanggilnya.

Itu berarti bahwa kita hanya dapat mengubah jenis pengembalian metode itu dari tugas<batal> menjadi winrt::Windows::Foundation::IAsyncAction (membiarkan sisanya tidak berubah).

winrt::Windows::Foundation::IAsyncAction Simple3DGame::LoadLevelAsync()
{
    m_level[m_currentLevel]->Initialize(m_objects);
    m_levelDuration = m_level[m_currentLevel]->TimeLimit() + m_levelBonusTime;
    co_return co_await m_renderer->LoadLevelResourcesAsync();
}

Sekarang seharusnya relatif mudah untuk memporting sisa metode tersebut, beserta dependensinya (seperti m_level, dan sebagainya), ke C++/WinRT.

GameRenderer::LoadLevelResourcesAsync

Berikut adalah versi C++/CX asli dari GameRenderer::LoadLevelResourcesAsync.

// GameConstants.h
namespace GameConstants
{
    ...
    static const int LevelLoadingDelay = 500;
    ...
}

// GameRenderer.cpp
task<void> GameRenderer::LoadLevelResourcesAsync()
{
    m_levelResourcesLoaded = false;

    return create_task([this]()
    {
        wait(GameConstants::LevelLoadingDelay);
    });
}

Simple3DGame::LoadLevelAsync adalah satu-satunya tempat dalam proyek yang memanggil GameRenderer::LoadLevelResourcesAsync, dan sudah menggunakan co_await untuk menyebutnya.

Jadi tidak ada lagi kebutuhan untuk GameRenderer::LoadLevelResourcesAsync untuk mengembalikan tugas —dapat mengembalikan winrt::Windows::Foundation::IAsyncAction sebagai gantinya. Dan implementasinya sendiri cukup sederhana untuk port sepenuhnya ke C++/WinRT. Ini melibatkan melakukan perubahan yang sama seperti yang kami lakukan pada Port concurrency::wait ke co_await winrt::resume_after. Dan tidak ada dependensi yang signifikan pada sisa proyek yang perlu dikhawatirkan.

Jadi, berikut adalah bagaimana metode terlihat setelah memindahkannya sepenuhnya ke C++/WinRT.

// GameConstants.h
namespace GameConstants
{
    using namespace std::literals::chrono_literals;
    ...
    static const auto LevelLoadingDelay = 500ms;
    ...
}

// GameRenderer.cpp
winrt::Windows::Foundation::IAsyncAction GameRenderer::LoadLevelResourcesAsync()
{
    m_levelResourcesLoaded = false;
    co_return co_await winrt::resume_after(GameConstants::LevelLoadingDelay);
}

Tujuan—sepenuhnya memindahkan metode ke C++/WinRT

Mari kita akhiri panduan ini dengan contoh hasil akhirnya, yaitu dengan memigrasikan sepenuhnya metode BasicReaderWriter::ReadDataAsync ke C++/WinRT.

Terakhir kali kita melihat metode ini (di bagian Port ReadDataAsync (sebagian besar) ke C++/WinRT, membiarkan sisa proyek tidak berubah), sebagian besar di-port ke C++/WinRT. Tetapi metode tersebut tetap mengembalikan objek bertipe Platform::Array<byte>^.

task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename)
{
    auto location_from_cx = from_cx<winrt::Windows::Storage::StorageFolder>(m_location);

    auto file = co_await location_from_cx.GetFileAsync(filename->Data());
    auto buffer = co_await winrt::Windows::Storage::FileIO::ReadBufferAsync(file);
    byte* bytes;
    auto byteAccess = buffer.as<Windows::Storage::Streams::IBufferByteAccess>();
    winrt::check_hresult(byteAccess->Buffer(&bytes));

    co_return ref new Platform::Array<byte>(bytes, buffer.Length());
}

Alih-alih mengembalikan tugas, kita akan mengubahnya untuk mengembalikan IAsyncOperation. Dan alih-alih mengembalikan array byte melalui IAsyncOperation tersebut, kita akan mengembalikan objek C++/WinRT IBuffer . Itu juga akan memerlukan perubahan kecil pada kode di situs panggilan, seperti yang akan kita lihat.

Berikut tampilan metode tersebut setelah implementasi, parameter, dan anggota data m_location dialihkan agar menggunakan sintaks dan objek C++/WinRT.

winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::Streams::IBuffer>
BasicReaderWriter::ReadDataAsync(
    _In_ winrt::hstring const& filename)
{
    StorageFile file{ co_await m_location.GetFileAsync(filename) };
    co_return co_await FileIO::ReadBufferAsync(file);
}

winrt::array_view<byte> BasicLoader::GetBufferView(
    winrt::Windows::Storage::Streams::IBuffer const& buffer)
{
    byte* bytes;
    auto byteAccess = buffer.as<Windows::Storage::Streams::IBufferByteAccess>();
    winrt::check_hresult(byteAccess->Buffer(&bytes));
    return { bytes, bytes + buffer.Length() };
}

Seperti yang Anda lihat, BasicReaderWriter::ReadDataAsync itu sendiri jauh lebih sederhana, karena kita telah memperhitungkan ke dalam metodenya sendiri logika sinkron yang mengambil byte dari buffer.

Tetapi sekarang kita perlu memindahkan situs panggilan dari struktur semacam ini di C++/CX.

task<void> BasicLoader::LoadTextureAsync(...)
{
    return m_basicReaderWriter->ReadDataAsync(filename).then(
        [=](const Platform::Array<byte>^ textureData)
    {
        CreateTexture(...);
    });
}

Untuk pola ini di C++/WinRT.

winrt::Windows::Foundation::IAsyncAction BasicLoader::LoadTextureAsync(...)
{
    auto textureBuffer = co_await m_basicReaderWriter.ReadDataAsync(filename);
    auto textureData = GetBufferView(textureBuffer);
    CreateTexture(...);
}

API penting