Asynchrony, dan interop antara C ++/WinRT dan C ++/CX

Tip

Meskipun kami sarankan Anda membaca topik ini dari awal, Anda dapat langsung melompat ke ringkasan teknik interop dalam Ikhtisar porting C ++/CX asinkron ke bagian C ++/WinRT .

Ini adalah topik lanjutan yang terkait dengan porting secara bertahap ke C ++/WinRT dari C ++/CX. Topik ini mengambil di mana topik Interop antara C ++/WinRT dan C ++/CX tinggalkan.

Jika ukuran atau kompleksitas basis kode Anda membuatnya perlu untuk port proyek Anda secara bertahap, maka Anda akan memerlukan proses porting di mana untuk sementara waktu C + + / CX dan C + + / kode WinRT ada berdampingan dalam proyek yang sama. Jika Anda memiliki kode asinkron, maka Anda mungkin perlu memiliki rantai tugas Parallel Patterns Library (PPL) dan coroutines ada berdampingan dalam proyek Anda saat Anda secara bertahap port kode sumber Anda. Topik ini berfokus pada teknik untuk interoperating 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 secara bertahap, terkontrol, di sepanjang jalan menuju porting seluruh proyek Anda, tanpa setiap perubahan kaskade tak terkendali sepanjang proyek.

Sebelum membaca topik ini, ada baiknya anda membaca Interop antara C++/WinRT dan C++/CX. Topik itu menunjukkan kepada Anda cara mempersiapkan proyek Anda untuk porting bertahap. Ini juga memperkenalkan dua fungsi pembantu yang dapat Anda gunakan untuk mengubah objek C ++/CX menjadi objek C ++/WinRT (dan sebaliknya). Topik tentang asynchrony ini didasarkan pada informasi itu, dan menggunakan fungsi pembantu tersebut.

Catatan

Ada beberapa batasan untuk porting secara bertahap dari C ++/CX ke C ++/WinRT. Jika Anda memiliki proyek komponen runtime Windows, maka porting secara bertahap tidak dimungkinkan, dan Anda harus port proyek dalam satu pass. Dan untuk proyek XAML, pada waktu tertentu jenis halaman XAML Anda harus semuanya C ++/WinRT atau semua C ++/CX. Untuk info 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 pindah dari tugas Parallel Patterns Library (PPL) ke coroutines. Modelnya berbeda. Tidak ada pemetaan satu-ke-satu alami dari tugas PPL ke coroutines, dan tidak ada cara sederhana (yang bekerja untuk semua kasus) untuk secara mekanis port kode.

Kabar baiknya adalah bahwa konversi dari tugas ke coroutines menghasilkan penyederhanaan yang signifikan. Dan tim pengembangan secara rutin melaporkan bahwa begitu mereka melewati rintangan untuk memindahkan 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 kebingungan yang tidak disengaja dari logika yang mendasarinya. Misalnya, loop menjadi rekursi; jika cabang lain berubah menjadi pohon bersarang (rantai) tugas; variabel bersama menjadi shared_ptr. Untuk mendekonstruksi struktur kode sumber PPL yang sering tidak alami, kami sarankan Anda terlebih dahulu mundur dan memahami maksud dari kode asli (yaitu, temukan versi sinkron asli). Dan kemudian masukkan co_await (kooperatif menunggu) ke tempat yang sesuai.

Oleh karena 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 memulai 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 porting ke C ++/ WinRT, struktur kode async Anda kemudian akan lebih mudah untuk port ke C #, jika Anda mau.

Beberapa latar belakang dalam pemrograman asinkron

Agar kita memiliki kerangka acuan umum untuk konsep dan terminologi pemrograman asinkron, mari kita atur secara singkat adegan mengenai pemrograman asinkron runtime Windows pada umumnya, dan juga bagaimana dua proyeksi bahasa C ++ masing-masing, dengan cara yang berbeda, berlapis di atas itu.

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

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

Windows Objek async Runtime (IAsyncXxx)

Ruang nama Runtime Windows Windows Foundation berisi empat jenis objek operasi asinkron.

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

Sinkronisasi C++/CX

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

Biasanya, metode C ++/CX asinkron rantai 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 bisa berupa 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 penelepon (mungkin file, array byte, atau Boolean).

Catatan

Jika metode C++/CX asinkron mengembalikan IAsyncXxx^, maka TResult (jika ada) terbatas untuk menjadi tipe runtime Windows. Nilai Boolean, misalnya, adalah tipe runtime Windows; tetapi tipe yang diproyeksikan C ++/CX (misalnya, Platform::Arraybyte<>^) tidak.

C++/WinRT async

C ++/WinRT mengintegrasikan coroutine C ++ ke dalam model pemrograman. Coroutines dan pernyataan itu co_await memberikan cara alami untuk kooperatif menunggu hasilnya.

Masing-masing jenis IAsyncXxx diproyeksikan menjadi tipe yang sesuai di namespace winrt::Windows::Foundation C++/WinRT. Mari kita sebut mereka sebagai winrt::IAsyncXxx (dibandingkan dengan IAsyncXxx^ 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, coroutine menggunakan co_return kata kunci untuk secara kooperatif mengembalikan nilai yang sebenarnya diinginkan penelepon (mungkin file, array byte, atau Boolean).

Jika suatu metode berisi setidaknya satu co_await pernyataan (atau setidaknya satu co_return atau co_yield), maka metode ini adalah coroutine karena alasan itu.

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

Sampel game Direct3D (Simple3DGameDX)

Topik ini berisi panduan dari beberapa teknik pemrograman tertentu yang menggambarkan bagaimana secara bertahap port kode asinkron. 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 itu dan secara bertahap port kode asinkronnya ke C ++/WinRT.

  • Unduh ZIP dari tautan di atas, dan buka ritsletingnya.
  • 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 dilakukan yang 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 kami akan bergantung pada fungsi pembantu tersebut dalam topik ini.
  • Akhirnya, tambahkan #include <pplawait.h> ke pch.h. Itu memberi Anda dukungan coroutine untuk PPL (ada lebih banyak tentang dukungan itu di bagian berikut).

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

  • BukaBasicLoader.cpp, dan komentar.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 itu.
  • Kemudian memenuhi syarat vektor sebagai std::vector, dan string sebagai std::string.

Proyek ini 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 dengan panduan kode dalam topik ini.

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

Singkatnya, saat kami port, kami akan mengubah rantai tugas PPL menjadi panggilan ke co_await. Kami akan mengubah nilai pengembalian metode dari tugas PPL menjadi objek winrt C++/WinRT ::IAsyncXxx . Dan kami juga akan mengubah IAsyncXxx^ apa pun menjadi winrt C++/WinRT ::IAsyncXxx.

Anda akan ingat bahwa coroutine adalah metode apa pun yang memanggil co_xxx. Coroutine C ++/WinRT digunakan 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 coroutine. 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 Bisakah kau co_await ? Bisakah kamu co_return darinya?
Metode mengembalikan taskvoid<> Ya Ya
Metode mengembalikan taskT<> 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 menggambarkan teknik interop yang menarik, atau hanya melanjutkan membaca dari sini.

Teknik interop Async Bagian dalam topik ini
Gunakan co_await untuk menunggu metode taskvoid<> dari dalam metode fire-and-forget, atau dalam konstruktor. Menunggu taskvoid<> dalam metode fire-and-forget
Gunakan co_await untuk menunggu metode taskvoid<> dari dalam metode taskvoid<>. Menunggu taskvoid<> dalam metode taskvoid<>
Gunakan co_await untuk menunggu metode taskvoid<> dari dalam metode taskT<>. Menunggu taskvoid<> dalam metode taskT<>
Gunakan co_await untuk menunggu metode IAsyncXxx^. Menunggu IAsyncXxx ^ dalam metode tugas , membuat sisa proyek tidak berubah
Gunakan co_return dalam metode taskvoid<>. Menunggu taskvoid<> dalam metode taskvoid<>
Gunakan co_return dalam metode taskT<>. Menunggu IAsyncXxx ^ dalam metode tugas , membuat sisa proyek tidak berubah
Bungkus create_async di sekitar tugas yang menggunakan co_return. Bungkus create_async di sekitar tugas yang menggunakan co_return
Konkurensi pelabuhan::tunggu. Konkurensi pelabuhan::tunggu untukco_await winrt::resume_after
Kembali winrt::IAsyncXxx bukan taskvoid<>. Port jenis pengembalian taskvoid<> ke winrt::IAsyncXxx
Konversi winrt::IAsyncXxxT<> (T adalah primitif) menjadi taskT<>. Konversi winrt::IAsyncXxxT<> (T adalah primitif) menjadi taskT<>
Konversi winrt::IAsyncXxxT<> (T adalah tipe Windows Runtime) menjadi taskT<^>. Mengonversi winrt::IAsyncXxxT<> (T adalah tipe Windows Runtime) menjadi taskT<^>

Dan inilah contoh kode singkat yang menggambarkan 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 operasi yang tidak mempengaruhi sisa proyek. Kami ingin menghindari menarik-naruk ujung longgar yang sewenang-wenang, dan dengan demikian mengungkap struktur seluruh proyek. Untuk itu, kita harus melakukan hal-hal dalam urutan tertentu. Selanjutnya kita akan melihat lebih dekat beberapa contoh membuat perubahan porting / interop terkait async semacam ini.

Menunggu metode taskvoid<>, meninggalkan sisa proyek tidak berubah

Metode yang mengembalikan taskvoid<> 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 async secara bertahap adalah untuk menemukan tempat-tempat di mana Anda menyebut metode tersebut. Tempat-tempat itu akan melibatkan pembuatan dan / atau mengembalikan tugas. Mereka mungkin juga melibatkan jenis rantai tugas di mana tidak ada nilai yang diteruskan dari setiap tugas ke kelanjutannya. Di tempat-tempat seperti itu, Anda bisa mengganti kode asinkron dengan co_await pernyataan, seperti yang akan kita lihat.

Catatan

Saat topik ini berlangsung, Anda akan melihat manfaat dari strategi ini. Setelah metode taskvoid<> tertentu dipanggil secara eksklusif melalui co_await, Anda kemudian bebas untuk port metode itu ke C ++/ WinRT, dan mengembalikan winrt::IAsyncXxx.

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

Penting

Dalam contoh berikut, ketika Anda melihat implementasi metode diubah, ingatlah bahwa kita tidak perlu mengubah penelepon metode yang kita ubah. Perubahan ini terlokalisasi, dan mereka tidak mengalir melalui proyek.

Menunggu taskvoid<> dalam metode fire-and-forget

Mari kita mulai dengan menunggu taskvoid<> dalam metode fire-and-forget, karena itu adalah kasus yang paling sederhana. Ini adalah metode yang bekerja secara asinkron, tetapi pemanggil metode tidak menunggu pekerjaan itu selesai. Anda cukup memanggil metode dan melupakannya, meskipun itu selesai secara asinkron.

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

Di Simple3DGameDX, Anda akan menemukan kode seperti itu dalam penerapan metode GameMain::Update. Itu ada di file GameMain.cppkode sumber .

GameMain::Perbarui

Berikut adalah ekstrak dari versi C ++/CX dari metode ini, menunjukkan dua bagian metode yang lengkap 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 taskvoid<> PPL). Setelah itu adalah kelanjutan yang melakukan beberapa pekerjaan sinkron. LoadLevelAsync 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 ini dijelaskan setelah daftar di bawah ini. Kita bisa berdiskusi di sini tentang cara yang aman untuk mengakses pointer ini di coroutine anggota kelas. Tapi mari kita tunda 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, kami bisa co_await melakukannya. Dan kita tidak memerlukan kelanjutan eksplisit—kode yang co_await mengikuti eksekusi hanya ketika LoadLevelAsync selesai.

Memperkenalkan mengubah co_await metode menjadi coroutine, jadi kami tidak bisa membiarkannya kembali void. Ini adalah metode api-dan-lupakan, jadi kami mengubahnya untuk mengembalikan winrt: fire_and_forget.

Anda juga harus mengedit GameMain.h. Ubah jenis pengembalian GameMain::Update dari voidwinrt::fire_and_forget dalam deklarasi di sana juga.

Anda dapat membuat perubahan ini pada salinan proyek Anda, dan permainan masih membangun dan menjalankan hal yang sama. Kode sumber pada dasarnya masih C ++/CX, tetapi sekarang menggunakan pola yang sama dengan C ++/WinRT, sehingga telah menggerakkan kita sedikit lebih dekat untuk dapat port sisa kode secara mekanis.

GameMain::ResetGame

GameMain::ResetGame adalah metode fire-and-forget lainnya; itu juga disebut LoadLevelAsync. Jadi Anda dapat membuat perubahan kode yang sama di sana jika Anda ingin latihan.

GameMain::OnDeviceRestored

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

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 voidwinrt::fire_and_forget in GameMain.h dan .cpp. Anda juga harus membuka DeviceResources.h dan membuat perubahan yang sama pada jenis pengembalian IDeviceNotify::OnDeviceRestored.

Untuk port kode async, hapus semua create_taskdan kemudian panggilan dan tanda kurung keriting mereka, dan sederhanakan metode menjadi serangkaian pernyataan datar.

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

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 async ini secara signifikan lebih sederhana, dan lebih mudah dibaca.

GameMain::GameMain

Konstruktor GameMain melakukan pekerjaan secara asinkron, dan tidak ada bagian dari proyek yang menunggu pekerjaan itu 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 kami akan memindahkan kode asinkron ke metode api-dan-lupakan GameMain: : ConstructInBackground baru, meratakan kode menjadi 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 api-dan-lupakan — pada kenyataannya, semua kode async — di GameMain telah diubah menjadi coroutines. Jika Anda merasa sangat cenderung, mungkin Anda bisa mencari metode api-dan-lupakan di kelas lain, dan membuat perubahan serupa.

Diskusi yang ditangguhkan tentang co_await dan petunjuk ini

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

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

Cerita pendeknya adalah bahwa solusinya adalah memanggil alat::get_strong. Tetapi untuk diskusi lengkap tentang masalah dan solusinya, lihat Dengan aman mengakses penunjuk ini dalam coroutine anggota kelas.

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

Dapatkan GameMain dari winrt::implements

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

class GameMain :
    public DX::IDeviceNotify

GameMain akan terus menerapkan DX::IDeviceNotify, tetapi kami akan mengubahnya untuk berasal dari winrt::implements.

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

Selanjutnya, pada App.cpptahun 2008, Anda akan menemukan metode ini.

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

Tapi sekarang GameMain berasal dari winrt: : implements, kita perlu membangunnya dengan cara yang berbeda. Dalam hal ini, kita akan menggunakan template fungsi winrt::make_self . Untuk info selengkapnya, lihat Jenis dan antarmuka implementasi Instantiating dan kembali.

Ganti baris kode itu dengan ini.

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

Untuk menutup loop pada perubahan itu, kita juga perlu mengubah jenis m_main. Pada App.htahun 2008, 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 ini.

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

Kita sekarang dapat memanggil mengimplementasikan::get_strong

Untuk GameMain::Update, dan untuk salah satu metode lain yang co_await kami tambahkan, inilah cara Anda dapat memanggil get_strong di awal coroutine untuk memastikan bahwa referensi yang kuat bertahan sampai coroutine selesai.

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

Menunggu taskvoid<> dalam metode taskvoid<>

Kasus paling sederhana berikutnya adalah menunggu taskvoid<> dalam metode yang dengan sendirinya mengembalikan taskvoid<>. Itu karena kita bisa co_awaitmenjadi taskvoid<>, dan kita bisa co_return dari satu.

Anda akan menemukan contoh yang sangat sederhana dalam penerapan metode Simple3DGame::LoadLevelAsync. Itu ada di 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, kita co_await itu, 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. Tapi sekarang kita memanggil GameRenderer::LoadLevelResourcesAsync via co_await, kita bebas untuk port itu untuk mengembalikan winrt::IAsyncXxx bukan tugas. Kita akan melakukannya nanti di bagian Port jenis pengembalian taskvoid<> ke winrt::IAsyncXxx.

Menunggu taskvoid<> dalam metode taskT<>

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

Baris pertama dalam contoh kode di bawah ini menunjukkan sederhananya co_awaitsebuah taskvoid<>. Kemudian, untuk memenuhi jenis pengembalian taskT<>, kita perlu mengembalikan StorageFile^secara asinkron. Untuk melakukan itu, kami co_await 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 bisa port lebih dari 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 itu.

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

Kami telah melihat bagaimana Anda dapat co_awaitmelakukan taskvoid<>. Anda juga co_await dapat melakukan metode yang mengembalikan IAsyncXxx, apakah itu metode dalam proyek Anda, atau API Windows asinkron (misalnya, StorageFolder.GetFileAsync, yang kami tunggu secara kooperatif 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 Windows API yang mengembalikan IAsyncXxx^. Tidak hanya itu, kita juga co_return dapat nilai yang basicreaderwriter::readdataAsync kembali secara asinkron (dalam hal ini, array byte). Langkah pertama ini menunjukkan bagaimana membuat perubahan itu; kami benar-benar akan port 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 penelepon metode yang kita ubah, karena kita tidak mengubah jenis pengembalian.

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

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

Satu-satunya dependensi yang dimiliki metode ini pada sisa proyek ini adalah BasicReaderWriter::m_location anggota data, yang merupakan C ++/CX StorageFolder^. Untuk membiarkan anggota data itu tidak berubah, dan untuk membiarkan jenis parameter dan tipe pengembalian tidak berubah, kita hanya perlu melakukan beberapa konversi—satu di awal metode, dan satu di akhir. Untuk itu, kita bisa menggunakan fungsi from_cx dan to_cx interop helper.

Berikut adalah bagaimana BasicReaderWriter::ReadDataAsync terlihat setelah porting implementasinya terutama ke C ++/WinRT. Ini adalah contoh yang baik dari porting secara bertahap. Dan metode ini berada pada tahap di mana kita dapat menjauh dari memikirkannya sebagai metode C ++/CX yang menggunakan beberapa teknik C ++/WinRT, dan melihatnya sebagai metode C ++/WinRT yang beroperasi 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

Di ReadDataAsync di atas, kami membangun dan mengembalikan array C ++/CX baru. Dan tentu saja kami melakukan itu untuk memenuhi jenis pengembalian metode (sehingga kami 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 hanyalah objek C ++/WinRT. Untuk co_return itu, panggil saja to_cx untuk mengonversinya. Ada info lebih lanjut tentang itu, dan contoh, bagian berikutnya.

Mengonversi winrt::IAsyncXxxT<> menjadi taskT<>

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

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

Konversi winrt::IAsyncXxxT<> (T adalah primitif) menjadi taskT<>

Pola di bagian ini berlaku saat Anda mengembalikan nilai primitif secara asinkron (kami akan menggunakan nilai Boolean untuk mengilustrasikan). Pertimbangkan contoh di mana metode yang telah Anda porting 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 itu 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 dari fungsi lambda bersifat eksplisit, karena kompiler tidak dapat menyimpulkannya.

Kita juga bisa menyebut metode dari dalam rantai tugas sewenang-wenang seperti ini. Sekali lagi, dengan tipe pengembalian lambda eksplisit.

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

Mengonversi winrt::IAsyncXxxT<> (T adalah tipe Windows Runtime) menjadi taskT<^>

Pola di bagian ini berlaku saat Anda mengembalikan nilai Runtime Windows secara asinkron (kami akan menggunakan nilai StorageFile untuk mengilustrasikan). Pertimbangkan contoh di mana metode yang telah Anda porting 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 menunjukkan cara mengonversi panggilan ke metode tersebut menjadi tugas. Perhatikan bahwa kita perlu memanggil fungsi to_cx interop helper untuk mengubah objek C ++/WinRT yang dikembalikan menjadi objek C ++/CX (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 ringkas dari itu.

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

Dan Anda bahkan dapat memilih untuk membungkus pola itu menjadi template fungsi yang dapat digunakan kembali, dan return itu seperti Biasanya Anda 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.

Bungkus create_async di sekitar tugas yang menggunakan co_return

Anda tidak co_return dapat 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 untuk 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 Anda bisa co_await.

Konkurensi pelabuhan::tunggu untukco_await winrt::resume_after

Ada beberapa tempat di mana Simple3DGameDX menggunakan konkurensi::tunggu untuk menjeda utas untuk waktu yang singkat. Berikut 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);
    }));
    ...
}

Konkurensi versi C++/WinRT ::wait adalah winrt::resume_after struct. Kita bisa co_await melakukan penataan 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 eksplisit, karena kompiler tidak lagi dapat menyimpulkannya.

Port jenis pengembalian taskvoid<> ke winrt::IAsyncXxx

Simple3DGame::LoadLevelAsync

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

Itu berarti bahwa kita dapat dengan mudah mengubah jenis pengembalian metode itu dari taskvoid<> ke 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 port sisa metode itu, dan dependensi (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 digunakan co_await untuk menyebutnya.

Jadi tidak perlu lagi GameRenderer::LoadLevelResourcesAsync untuk mengembalikan tugas—game ini 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 di Port concurrency::wait to co_await winrt::resume_after. Dan tidak ada ketergantungan yang signifikan pada sisa proyek yang perlu dikhawatirkan.

Jadi inilah cara metode ini terlihat setelah porting 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);
}

Tujuannya—sepenuhnya port metode ke C ++/WinRT

Mari kita selesaikan panduan ini dengan contoh tujuan akhir, dengan sepenuhnya porting metode BasicReaderWriter::ReadDataAsync ke C ++/WinRT.

Terakhir kali kami melihat metode ini (di bagian Port ReadDataAsync (sebagian besar) ke C ++/WinRT, meninggalkan sisa proyek tidak berubah), sebagian besar porting ke C ++/WinRT. Tapi itu masih mengembalikan tugas Platform::Arraybyte<>^.

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, kami akan mengubahnya untuk mengembalikan IAsyncOperation. Dan alih-alih mengembalikan array byte melalui IAsyncOperation itu, kami malah akan mengembalikan objek C ++/WinRT IBuffer . Itu juga akan membutuhkan perubahan kecil pada kode di situs panggilan, seperti yang akan kita lihat.

Inilah cara metode ini terlihat setelah porting implementasinya, parameternya, dan m_location anggota data 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 kami telah memperhitungkan metodenya sendiri logika sinkron yang mengambil byte dari buffer.

Tapi sekarang kita perlu port 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