Asinkron, dan interop 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 mengambil tempat topik Interop antara C++/WinRT dan C++/CX tidak aktif.

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, maka Anda mungkin perlu memiliki rantai tugas Paralel Patterns Library (PPL) dan koroutine ada berdampingan dalam proyek Anda saat Anda secara bertahap mem-port kode sumber Anda. 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 ini memungkinkan Anda untuk membuat perubahan lokal bertahap, terkontrol di sepanjang jalur menuju porting seluruh proyek Anda, tanpa memiliki setiap perubahan yang tidak terkendali di 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.

Catatan

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 waktu tertentu jenis halaman XAML Anda harus semua C++/WinRT atau semua C++/CX. Untuk informasi selengkapnya, lihat topik Pindah ke C++/WinRT dari C++/CX.

Alasan seluruh topik didedikasikan untuk interop kode asinkron

Porting dari C++/CX ke C++/WinRT umumnya mudah, dengan satu pengecualian berpindah dari tugas Parallel Patterns Library (PPL) ke koroutine. 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 mereka lebih dari rintangan porting kode asinkron mereka, sisa pekerjaan porting sebagian besar 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). Dan kemudian masukkan co_await (secara kooperatif menunggu) ke tempat-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

Sehingga kita memiliki bingkai referensi umum untuk konsep dan terminologi pemrograman asinkron, mari kita atur adegan secara singkat mengenai pemrograman asinkron Windows Runtime secara umum, dan juga bagaimana kedua proyeksi bahasa C++ masing-masing, dengan cara yang berbeda, berlapis 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 hal ini lebih efisien bagi metode asinkron untuk tidak mengembalikan objek operasi asinkron. Metode asinkron seperti itu—metode yang tidak Anda tunggu—dikenal sebagai metode fire-and-forget .

Objek asinkron Windows Runtime (IAsyncXxx)

Namespace Windows::Foundation Windows Runtime 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.

Asinkron C++/CX

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

Biasanya, metode C++/CX asinkron menautkan tugas PPL bersama-sama dengan menggunakan fungsi lambda dengan konkurensi::create_task dan konkurensi::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).

Catatan

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

Asinkron C++/WinRT

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

Setiap jenis IAsyncXxx diproyeksikan ke dalam jenis yang sesuai di namespace winrt::Windows::Foundation C++/WinRT. 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 return kata kunci untuk mengembalikan objek asinkron, koroutine menggunakan co_return kata kunci untuk secara kooperatif mengembalikan nilai yang sebenarnya diinginkan pemanggil (mungkin file, array byte, atau Boolean).

Jika metode berisi setidaknya satu co_await pernyataan (atau setidaknya satu co_return atau co_yield), maka metode ini adalah koroutine karena alasan tersebut.

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

Sampel game 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. Itu memberi Anda dukungan coroutine untuk PPL (ada lebih banyak tentang dukungan tersebut di bagian berikut).

Jangan membangunnya, jika tidak, Anda akan mendapatkan kesalahan tentang byte yang ambigu. Berikut cara mengatasinya.

  • Buka BasicLoader.cpp, dan komentari using namespace std;.
  • 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 dibangun lagi, memiliki dukungan C++/WinRT, dan berisi fungsi pembantu interop 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 port, kita akan mengubah rantai 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 akan 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 (milik pplawait.h), Anda juga dapat menggunakan co_return untuk mengembalikan tugas PPL dari koroutine. Dan Anda juga co_await dapat melakukan tugas dan IAsyncXxx. Tetapi Anda tidak dapat menggunakan co_return untuk mengembalikan IAsyncXxx^. Tabel di bawah ini menjelaskan dukungan untuk interop antara berbagai teknik asinkron dengan pplawait.h dalam gambar.

Metode Dapatkah Anda co_await itu? Dapatkah Anda co_return dari itu?
Metode mengembalikan kekosongan tugas<> Ya Ya
Metode mengembalikan tugas<T> Tidak Ya
Metode mengembalikan IAsyncXxx^ Ya Nomor. Tetapi Anda membungkus create_async di sekitar tugas yang menggunakan co_return.
Metode mengembalikan winrt::IAsyncXxx Ya Ya

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 menunggu metode kekosongan> tugas<dari dalam metode fire-and-forget, atau dalam konstruktor. Menunggu tugas<batal> dalam metode fire-and-forget
Gunakan co_await untuk menunggu metode kekosongan> tugas<dari dalam metode kekosongan> tugas<. Menunggu tugas batal> dalam metode kekosongan tugas><<
Gunakan co_await untuk menunggu metode kekosongan> tugas<dari dalam metode tugas<T>. Menunggu tugas batal> dalam metode tugas T><<
Gunakan co_await untuk menunggu metode IAsyncXxx^. Menunggu IAsyncXxx^ dalam metode tugas , membiarkan sisa proyek tidak berubah
Gunakan co_return dalam metode kekosongan> tugas<. Menunggu tugas batal> dalam metode kekosongan tugas><<
Gunakan co_return dalam metode T> tugas<. 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. Konkurensi port::wait to co_await winrt::resume_after
Mengembalikan winrt::IAsyncXxx alih-alih tugas<batal>. Port jenis pengembalian void> tugas<ke winrt::IAsyncXxx
Mengonversi winrt::IAsyncXxx<T> (T primitif) ke tugas<T>. Mengonversi winrt::IAsyncXxx<T> (T primitif) ke tugas<T>
Mengonversi winrt::IAsyncXxx<T> (T adalah jenis Windows Runtime) ke 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();
}

Penting

Bahkan dengan opsi interop yang hebat ini, porting secara bertahap tergantung pada memilih perubahan yang dapat kita buat secara bedah yang tidak memengaruhi sisa proyek. Kami ingin menghindari tudingan pada ujung longgar yang segan- segan, dan dengan demikian mengurai struktur seluruh 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.

Menunggu metode kekosongan> tugas<, membiarkan sisa proyek tidak berubah

Metode yang mengembalikan void> tugas<melakukan pekerjaan 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 mungkin juga melibatkan jenis rantai tugas di mana tidak ada nilai yang diteruskan dari setiap tugas kelanjutannya. Di tempat-tempat seperti itu, Anda hanya dapat mengganti kode asinkron dengan co_await pernyataan, seperti yang akan kita lihat.

Catatan

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

Penting

Dalam contoh berikut, seperti yang Anda lihat implementasi metode yang diubah, ingatlah bahwa kita tidak perlu mengubah pemanggil metode yang kita ubah. Perubahan ini dilokalkan, dan tidak kaskade melalui proyek.

Menunggu tugas<batal> dalam metode fire-and-forget

Mari kita mulai dengan menunggu tugas<batal> dalam metode fire-and-forget , karena itulah kasus paling sederhana. Ini adalah metode yang bekerja secara asinkron, tetapi pemanggil metode tidak menunggu pekerjaan tersebut selesai. Anda hanya memanggil metode dan melupakannya, terlepas dari kenyataan bahwa itu selesai secara asinkron.

Lihat ke akar grafik dependensi proyek Anda untuk void metode yang berisi create_task dan/atau rantai tugas di mana hanya metode kekosongan> tugas<yang dipanggil.

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 panggilan ke metode Simple3DGame::LoadLevelAsync (yang mengembalikan kekosongan> tugas<PPL). Setelah itu adalah kelanjutan yang melakukan beberapa pekerjaan 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. Kode dijelaskan setelah daftar 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.

Memperkenalkan co_await mengubah metode menjadi coroutine, sehingga kami tidak dapat membiarkannya kembali void. Ini adalah metode fire-and-forget, jadi kami mengubahnya untuk 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 membuat perubahan ini pada salinan proyek Anda, dan permainan masih membangun dan menjalankan yang sama. 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; juga memanggil LoadLevelAsync. Jadi Anda dapat membuat perubahan kode yang sama di sana jika Anda ingin berlatih.

GameMain::OnDeviceRestored

Hal-hal menjadi sedikit lebih menarik di GameMain::OnDeviceRestored karena bersarangnya kode asinkron yang lebih dalam, termasuk tugas tanpa operasi. 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 memindahkan kode asinkron, hapus semua create_task lalu panggil dan tanda kurung kurawalnya, dan sederhanakan metode menjadi serangkaian pernyataan datar.

Ubah apa pun return yang mengembalikan tugas menjadi co_await. Anda akan dibiarkan dengan yang return tidak mengembalikan apa-apa, jadi hapus saja. Setelah selesai, tugas no-op akan menghilang, dan kerangka bagian asinkron dari metode akan terlihat seperti ini. Sekali lagi, kode sinkron yang kurang menarik diteruskan.

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—pada kenyataannya, semua kode asinkron— di GameMain telah diubah menjadi koroutin. Jika Anda merasa begitu cenderung, mungkin Anda dapat mencari metode fire-and-forget di kelas lain, dan membuat perubahan serupa.

Diskusi yang ditangguhkan tentang co_await dan penunjuk ini

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

Ini berlaku untuk semua metode yang telah kita ubah sejauh ini; dan berlaku untuk semua koroutin, bukan hanya api-dan-lupakan satu. Memperkenalkan co_await ke dalam metode memperkenalkan titik penangguhan. Dan karena itu, kita harus berhati-hati dengan pointer ini , yang tentu saja kita manfaatkan setelah titik penangguhan setiap kali kita mengakses anggota kelas.

Singkatnya adalah bahwa solusinya adalah memanggil implements::get_strong. Tetapi untuk diskusi lengkap tentang masalah dan solusinya, lihat Brankas mengakses pointer ini dalam koroutine 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 instans dan mengembalikan jenis dan antarmuka implementasi.

Ganti baris kode tersebut dengan ini.

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

Untuk menutup perulangan pada perubahan tersebut, kita juga perlu mengubah jenis 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 salah satu metode lain yang kami tambahkan co_await , inilah cara Anda dapat memanggil get_strong di awal koroutine untuk memastikan bahwa referensi yang kuat bertahan sampai koroutin selesai.

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

Menunggu tugas batal> dalam metode kekosongan tugas><<

Kasus paling sederhana berikutnya adalah menunggu tugas batal> dalam metode yang mengembalikan tugas<batal>.< 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 kita memanggil GameRenderer::LoadLevelResourcesAsync melalui co_await, kita bebas untuk memindahkannya untuk mengembalikan winrt::IAsyncXxx alih-alih tugas. Kita akan melakukannya nanti di bagian Port jenis pengembalian void> tugas<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 kekosongan> tugas<sederhana co_await. Kemudian, untuk memenuhi jenis pengembalian T> tugas<, kita perlu mengembalikan StorageFile^secara asinkron. Untuk melakukannya, kami co_await adalah Windows Runtime API, dan co_return file 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

Kami telah melihat bagaimana Anda dapat tugas batalco_await>.< Anda juga co_await dapat menggunakan metode yang mengembalikan IAsyncXxx, apakah itu metode dalam proyek Anda, atau API Windows asinkron (misalnya, StorageFolder.GetFileAsync, yang secara kooperatif kami tunggu di 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 api Windows yang mengembalikan IAsyncXxx^. Tidak hanya itu, kita juga co_return dapat nilai bahwa BasicReaderWriter::ReadDataAsync mengembalikan secara asinkron (dalam hal ini, array 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 metode yang kita ubah, karena kita tidak mengubah jenis pengembalian.

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 tidak berubah, dan untuk membiarkan jenis parameter dan mengembalikan jenis yang tidak berubah, kita hanya perlu melakukan beberapa 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 dari 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());
}

Catatan

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.

Mengonversi winrt::IAsyncXxx<T> ke tugas<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^.

Mengonversi winrt::IAsyncXxx<T> (T primitif) ke tugas<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 jenis pengembalian tugas fungsi lambda eksplisit, karena pengkompilasi tidak dapat menyimpulkannya.

Kita juga dapat memanggil metode dari dalam rantai tugas arbitrer seperti ini. Sekali lagi, dengan jenis pengembalian lambda 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 secara asinkron mengembalikan nilai Windows Runtime (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 C++/CX handle (juga dikenal sebagai topi).

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 itu ke dalam templat fungsi yang dapat digunakan kembali, dan return seperti biasanya Anda akan mengembalikan tugas.

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 co_return dapat menggunakan IAsyncXxx^ secara langsung, tetapi Anda dapat mencapai 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 bisa mendapatkan nilai pengembalian dari metode apa pun yang Dapat co_awaitAnda .

Konkurensi port::wait to co_await winrt::resume_after

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

// 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 konkurensi::wait adalah struktur winrt::resume_after . Kita dapat co_await melakukan struct 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 eksplisit, karena pengkompilasi tidak lagi dapat menyimpulkannya.

Port jenis pengembalian void> tugas<ke winrt::IAsyncXxx

Simple3DGame::LoadLevelAsync

Pada tahap ini dalam pekerjaan kami dengan Simple3DGameDX, semua tempat dalam proyek yang memanggil Simple3DGame::LoadLevelAsync gunakan co_await untuk menyebutnya.

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 harus cukup mekanis untuk memindahkan sisa metode itu, dan 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. Itu melibatkan membuat perubahan yang sama yang kami buat dalam Konkurensi port ::tunggu hingga 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 bungkus panduan ini dengan contoh tujuan akhir, dengan sepenuhnya memindahkan 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 masih mengembalikan tugas 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 adalah bagaimana metode terlihat setelah memindahkan implementasinya, parameternya, dan anggota data m_location untuk 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