Speciális egyidejűség és aszinkronság a C++/WinRT használatával

Ez a témakör a C++/WinRT-ben egyidejűséggel és aszinkron működéssel rendelkező speciális forgatókönyveket ismerteti.

A témakör bemutatásához először olvassa el az egyidejűséget és az aszinkron műveleteket.

Feladatok áthelyezése a Windows szálkészletére

A korutin ugyanúgy függvény, mint bármely más: a hívó blokkolva van, amíg a függvény vissza nem adja neki a vezérlést. A korutin először az első co_await, co_return vagy co_yield esetén térhet vissza.

Tehát mielőtt számításigényes munkát végezne egy korutinban, vissza kell adnia a vezérlést a hívónak (más szóval be kell vezetnie egy felfüggesztési pontot), hogy a hívó ne blokkolódjon. Ha még nem végzi el ezt co_awaitegy másik művelettel, akkor a co_await függvényt használhatja. Ez visszaadja a vezérlést a hívónak, majd azonnal folytatja a végrehajtást a szálkészlet egyik szálán.

A megvalósítás során használt szálkészlet az alacsony szintű Windows szálkészlet, így optimálisan hatékony.

IAsyncOperation<uint32_t> DoWorkOnThreadPoolAsync()
{
    co_await winrt::resume_background(); // Return control; resume on thread pool.

    uint32_t result;
    for (uint32_t y = 0; y < height; ++y)
    for (uint32_t x = 0; x < width; ++x)
    {
        // Do compute-bound work here.
    }
    co_return result;
}

Programozás a szálaffinitást szem előtt tartva

Ez a forgatókönyv az előzőre épül. A munka egy részét a szálkészletre irányítja, de a felhasználói felületen (UI) szeretné megjeleníteni a folyamat előrehaladását.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    textblock.Text(L"Done!"); // Error: TextBlock has thread affinity.
}

A fenti kód egy winrt::hresult_wrong_thread kivételt eredményez, mert egy TextBlock-et frissíteni kell az azt létrehozó szálról, amely a felhasználói felületi szál. Az egyik megoldás az, hogy rögzítjük azt a szálkörnyezetet, amelyben a koroutint eredetileg meghívták. Ehhez hozzon létre egy winrt::apartment_context objektumot, végezze el a háttérben végzendő munkát, majd a apartment_contextco_await használatával váltson vissza a hívó környezetre.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    winrt::apartment_context ui_thread; // Capture calling context.

    co_await winrt::resume_background();
    // Do compute-bound work here.

    co_await ui_thread; // Switch back to calling context.

    textblock.Text(L"Done!"); // Ok if we really were called from the UI thread.
}

Ha a fenti coroutine a TextBlockot létrehozó felhasználói felületi szálból van meghívva, akkor ez a technika működik. Az alkalmazásban sok olyan eset lesz, ahol biztos lehet benne.

Ha általánosabb megoldást szeretne a felhasználói felület frissítésére, amely olyan esetekre terjed ki, amikor nem biztos a hívó szálban, a co_await függvényrel válthat egy adott előtérszálra. Az alábbi kódpéldában a TextBlock-hoz társított diszpécsersor átadásával adjuk meg az előtérszálat (a DispatcherQueue tulajdonságának elérésével). A winrt::resume_foreground meghívja a DispatcherQueue.TryEnqueue parancsot az adott kézbesítői üzenetsor-objektumon, hogy végrehajtsa az azt követő munkát a coroutine-ban.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    // Switch to the foreground thread associated with textblock.
    co_await winrt::resume_foreground(textblock.DispatcherQueue());

    textblock.Text(L"Done!"); // Guaranteed to work.
}

A winrt::resume_foreground függvény egy választható prioritási paramétert vesz igénybe. Ha ezt a paramétert használja, akkor a fent látható minta megfelelő. Ha nem, akkor dönthet úgy, hogy egyszerűen co_await winrt::resume_foreground(someDispatcherObject);leegyszerűsítico_await someDispatcherObject;.

Végrehajtási környezetek, folytatás és váltás egy coroutine-ban

Általánosságban elmondható, hogy egy korutin felfüggesztési pontja után az eredeti végrehajtási szál megszűnhet, és a végrehajtás bármely szálon folytatódhat (más szóval bármely szál meghívhatja az aszinkron művelet Completed metódusát).

Ha azonban co_await a négy Windows-futtatókörnyezet aszinkron művelettípus (IAsyncXxx) bármelyikét, akkor a C++/WinRT rögzíti a hívási környezetet akkor, amikor co_await. És ez biztosítja, hogy továbbra is abban a kontextusban maradjon, amikor a folytatás újraindul. A C++/WinRT ezt úgy valósítja meg, hogy ellenőrzi, már a hívási kontextusban van-e, és ha nem, átvált arra. Ha a co_await előtt egyszálas apartman- (STA-) szálon volt, akkor utána is ugyanazon a szálon lesz; ha a co_await előtt többszálas apartman- (MTA-) szálon volt, akkor utána is egy ilyen szálon lesz.

IAsyncAction ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;

    // The thread context at this point is captured...
    SyndicationFeed syndicationFeed{ co_await syndicationClient.RetrieveFeedAsync(rssFeedUri) };
    // ...and is restored at this point.
}

Ennek a viselkedésnek az az oka, hogy a C++/WinRT kódot biztosít az Windows-futtatókörnyezet aszinkron művelettípusoknak a C++ coroutine nyelvi támogatáshoz való igazításához (ezeket a kódrészleteket várakozási adaptereknek nevezzük). A C++/WinRT többi várható típusa egyszerűen a szálkészlet burkolóosztályai és/vagy segítői; ezért a szálkészletben fejeződnek be.

using namespace std::chrono_literals;
IAsyncOperation<int> return_123_after_5s()
{
    // No matter what the thread context is at this point...
    co_await 5s;
    // ...we're on the thread pool at this point.
    co_return 123;
}

Ha co_await más típusú – akár C++/WinRT coroutine implementáción belül is –, akkor egy másik kódtár biztosítja az adaptereket, és meg kell értenie, hogy ezek az adapterek mit tesznek az újrakezdés és a környezetek szempontjából.

A kontextusváltások számának minimálisra csökkentéséhez használhatja az ebben a témakörben már bemutatott technikák némelyikét. Lássunk néhány illusztrációt erről. Ebben a következő pszeudokód-példában egy eseménykezelő körvonalát mutatjuk be, amely meghív egy Windows-futtatókörnyezet API-t egy kép betöltéséhez, egy háttérszálra vetül a kép feldolgozásához, majd visszatér a felhasználói felületi szálhoz, hogy megjelenítse a képet a felhasználói felületen.

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    // Call StorageFile::OpenAsync to load an image file.

    // The call to OpenAsync occurred on a background thread, but C++/WinRT has restored us to the UI thread by this point.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Process the image.

    co_await winrt::resume_foreground(this->DispatcherQueue());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

Ebben a forgatókönyvben a StorageFile::OpenAsync hívása körül van egy kis hatékonysági hiba. Szükség van egy háttérszálra való környezeti váltásra (hogy a kezelő vissza tudja adni a végrehajtást a hívónak), az újrakezdéskor, amely után a C++/WinRT visszaállítja a felhasználói felületi szál környezetét. Ebben az esetben azonban nem szükséges a felhasználói felületi szálon lennie, amíg nem készülünk frissíteni a felhasználói felületet. Minél több Windows-futtatókörnyezet API-t hívunk meg awinrt::resume_background hívásunk előtt, annál több szükségtelen oda-vissza kontextusváltást kell elszenvednünk. A megoldás az, hogy előtte nem hívunk meg Windows-futtatókörnyezet API-kat. Helyezze át az összeset a winrt::resume_background után.

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Call StorageFile::OpenAsync to load an image file.

    // Process the image.

    co_await winrt::resume_foreground(this->DispatcherQueue());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

Ha valami fejlettebbet szeretne csinálni, akkor írhat saját await adaptereket. Ha például azt szeretné, hogy a co_await ugyanazon a szálon folytatódjon, amelyen az aszinkron művelet befejeződik (tehát nincs kontextusváltás), akkor kezdheti azzal, hogy az alább bemutatottakhoz hasonló await adaptereket ír.

Note

Az alábbi példakód kizárólag oktatási célokat szolgál; arra szolgál, hogy segítsen megérteni, hogyan működnek az await adapterek. Ha ezt a technikát a saját kódbázisában szeretné használni, azt javasoljuk, hogy fejlessze ki és tesztelje a saját await adapterstruktúrá(ka)t. Írhat például complete_on_any, complete_on_current és complete_on(diszpécser). Érdemes lehet olyan sablonokat is készíteni, amelyek az IAsyncXxx típust sablonparaméterként veszik fel.

struct no_switch
{
    no_switch(Windows::Foundation::IAsyncAction const& async) : m_async(async)
    {
    }

    bool await_ready() const
    {
        return m_async.Status() == Windows::Foundation::AsyncStatus::Completed;
    }

    void await_suspend(std::experimental::coroutine_handle<> handle) const
    {
        m_async.Completed([handle](Windows::Foundation::IAsyncAction const& /* asyncInfo */, Windows::Foundation::AsyncStatus const& /* asyncStatus */)
        {
            handle();
        });
    }

    auto await_resume() const
    {
        return m_async.GetResults();
    }

private:
    Windows::Foundation::IAsyncAction const& m_async;
};

A no_switch váró adapterek használatának megértéséhez először tudnia kell, hogy amikor a C++ fordító egy kifejezéssel találkozik co_await , await_ready, await_suspend és await_resume nevű függvényeket keres. A C++/WinRT kódtár biztosítja ezeket a függvényeket, hogy alapértelmezés szerint ésszerű viselkedést kapjon, például így.

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

A no_switch await adapterek használatához egyszerűen módosítsa annak a co_await kifejezésnek a típusát IAsyncXxx-ről no_switch-re, így.

IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);

Ezután az IAsyncXxx-nek megfelelő három await_xxx függvény helyett a C++ fordító olyan függvényeket keres, amelyek megfelelnek no_switch.

A winrt részletesebb bemutatása::resume_foreground

A C++/WinRT 2.0-tól a winrt::resume_foreground függvény akkor is felfüggeszti, ha a diszpécserszálról hívják (a korábbi verziókban holtpontot okozhat bizonyos esetekben, mert csak akkor függesztette fel, ha még nem volt a diszpécserszálon).

Az aktuális viselkedés azt jelenti, hogy támaszkodhat a verem visszatekerésére és újrasorolására; és ez fontos a rendszer stabilitása szempontjából, különösen az alacsony szintű rendszerkódok esetében. A fent említett, Programozás a szálaffinitás figyelembevételével című szakasz utolsó kódpéldája azt szemlélteti, hogyan lehet egy összetett számítást háttérszálon elvégezni, majd a felhasználói felület (UI) frissítéséhez a megfelelő felhasználóifelület-szálra átváltani.

Így néz ki a winrt::resume_foreground belsőleg.

auto resume_foreground(...) noexcept
{
    struct awaitable
    {
        bool await_ready() const
        {
            return false; // Queue without waiting.
            // return m_dispatcher.HasThreadAccess(); // The C++/WinRT 1.0 implementation.
        }
        void await_resume() const {}
        void await_suspend(coroutine_handle<> handle) const { ... }
    };
    return awaitable{ ... };
};

Ez az aktuális és az előző viselkedés hasonló a PostMessage és a SendMessage közötti különbséghez a Win32-alkalmazásfejlesztésben. A PostMessage várólistára állítja a munkát, majd anélkül oldja fel a vermet, hogy megvárja a munka befejezését. A verem kibontása elengedhetetlen lehet.

A winrt::resume_foreground függvény eredetileg támogatta a CoreDispatchert (amely egy CoreWindow-hoz kötődik), amelyet a Windows 10 előtt vezettek be. A WinUI 3 és Windows App SDK alkalmazásokban használja inkább a DispatcherQueue-t. A DispatcherQueue-t saját céljaira is létrehozhatja. Fontolja meg ezt az egyszerű konzolalkalmazást.

using namespace Windows::System;

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    auto controller{ DispatcherQueueController::CreateOnDedicatedThread() };
    RunAsync(controller.DispatcherQueue());
    getchar();
}

A fenti példa létrehoz egy (vezérlőn belüli) üzenetsort egy privát szálon, majd átadja a vezérlőt a koroutinnak. A coroutine az üzenetsor használatával várakozhat (felfüggesztheti és folytathatja) a privát szálon. A DispatcherQueue másik gyakori használata egy üzenetsor létrehozása az aktuális felhasználói felületi szálon egy hagyományos asztali vagy Win32-alkalmazáshoz.

DispatcherQueueController CreateDispatcherQueueController()
{
    DispatcherQueueOptions options
    {
        sizeof(DispatcherQueueOptions),
        DQTYPE_THREAD_CURRENT,
        DQTAT_COM_STA
    };
 
    ABI::Windows::System::IDispatcherQueueController* ptr{};
    winrt::check_hresult(CreateDispatcherQueueController(options, &ptr));
    return { ptr, take_ownership_from_abi };
}

Ez bemutatja, hogyan hívhatja meg és építheti be a Win32-függvényeket a C++/WinRT-projektekbe úgy, hogy egyszerűen meghívja a Win32 stílusú CreateDispatcherQueueController függvényt a vezérlő létrehozásához, majd a kapott üzenetsor-vezérlő tulajdonjogát WinRT-objektumként továbbítja a hívónak. A meglévő Petzold-stílusú Win32 asztali alkalmazáson is pontosan így támogathatja a hatékony és zökkenőmentes sorba állítást.

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    Window window;
    auto controller{ CreateDispatcherQueueController() };
    RunAsync(controller.DispatcherQueue());
    MSG message;
 
    while (GetMessage(&message, nullptr, 0, 0))
    {
        DispatchMessage(&message);
    }
}

Fent az egyszerű függvény egy ablak létrehozásával kezdődik. El tudja képzelni, hogy ez regisztrál egy ablakosztályt, és meghívja a CreateWindow-t a legfelső szintű asztali ablak létrehozásához. A Rendszer ezután meghívja a CreateDispatcherQueueController függvényt az üzenetsor-vezérlő létrehozásához, mielőtt meghívna egy coroutine-t a vezérlő tulajdonában lévő kézbesítő üzenetsorsal. Ezután egy hagyományos üzenetszivattyú indul el, amelyben a korutin folytatása természetes módon ezen a szálon történik. Ezzel visszatérhet a coroutines elegáns világához az aszinkron vagy üzenetalapú munkafolyamathoz az alkalmazásban.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ... // Begin on the calling thread...
 
    co_await winrt::resume_foreground(queue);
 
    ... // ...resume on the dispatcher thread.
}

A winrt::resume_foreground hívása mindig várólistára kerül, majd kikapcsolja a vermet. Igény szerint beállíthatja az újrakezdési prioritást is.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
 
    ...
}

Vagy használja az alapértelmezett sorbanállási sorrendet.

...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await queue;
 
    ...
}

Note

Ahogy fent látható, mindenképpen adja meg annak a névtérnek a projection fejlécét, amelyhez az éppen co_await-olt típus tartozik. Például Windows::System::DispatcherQueue vagy Microsoft::UI::Dispatching::DispatcherQueue.

Vagy ebben az esetben az üzenetsor leállásának észlelése és annak szabályos kezelése.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    if (co_await queue)
    {
        ... // Resume on dispatcher thread.
    }
    else
    {
        ... // Still on calling thread.
    }
}

A(z) co_await kifejezés eredménye true, ami azt jelzi, hogy a folytatás a dispatcher szálon történik. Más szóval ez a sorba állítás sikeres volt. Ezzel szemben false értékkel tér vissza, jelezve, hogy a végrehajtás a hívó szálon marad, mert az üzenetsor vezérlője éppen leáll, és már nem szolgálja ki az üzenetsorral kapcsolatos kéréseket.

Szóval, óriási erő van a kezedben, amikor a C++/WinRT-t korutinokkal kombinálod; különösen akkor, ha régi vágású, Petzold-féle asztali alkalmazásokat fejlesztesz.

Aszinkron művelet megszakítása és lemondási visszahívások

Az Windows-futtatókörnyezet aszinkron programozással kapcsolatos funkciói lehetővé teszik egy repülés közbeni aszinkron művelet vagy művelet megszakítását. Íme egy példa, amely meghívja a StorageFolder::GetFilesAsync parancsot egy potenciálisan nagy fájlgyűjtemény lekérésére, és az eredményül kapott aszinkron műveleti objektumot egy adattagban tárolja. A felhasználónak lehetősége van megszakítani a műveletet.

// MainPage.xaml
...
<Button x:Name="workButton" Click="OnWork">Work</Button>
<Button x:Name="cancelButton" Click="OnCancel">Cancel</Button>
...

// MainPage.h
...
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Storage.Search.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Storage;
using namespace Windows::Storage::Search;
using namespace Microsoft::UI::Xaml;
...
struct MainPage : MainPageT<MainPage>
{
    MainPage()
    {
        InitializeComponent();
    }

    IAsyncAction OnWork(IInspectable /* sender */, RoutedEventArgs /* args */)
    {
        workButton().Content(winrt::box_value(L"Working..."));

        // Enable the Pictures Library capability in the app manifest file.
        StorageFolder picturesLibrary{ KnownFolders::PicturesLibrary() };

        m_async = picturesLibrary.GetFilesAsync(CommonFileQuery::OrderByDate, 0, 1000);

        IVectorView<StorageFile> filesInFolder{ co_await m_async };

        workButton().Content(box_value(L"Done!"));

        // Process the files in some way.
    }

    void OnCancel(IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
    {
        if (m_async.Status() != AsyncStatus::Completed)
        {
            m_async.Cancel();
            workButton().Content(winrt::box_value(L"Canceled"));
        }
    }

private:
    IAsyncOperation<::IVectorView<StorageFile>> m_async;
};
...

A lemondás implementálási oldalán kezdjük egy egyszerű példával.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction ImplicitCancelationAsync()
{
    while (true)
    {
        std::cout << "ImplicitCancelationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto implicit_cancelation{ ImplicitCancelationAsync() };
    co_await 3s;
    implicit_cancelation.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

Ha a fenti példát futtatja, akkor az ImplicitCancelationAsync másodpercenként egy üzenetet fog nyomtatni három másodpercre, amely után a megszakítás következtében automatikusan leáll. Ez azért működik, mert amikor egy co_await kifejezéssel találkozik, a koroutin ellenőrzi, hogy a kifejezés törölve lett-e. Ha igen, akkor azonnal kilép; ha nem, akkor a szokott módon felfüggesztődik.

A lemondás természetesen előfordulhat, amíg a koroutin fel van függesztve. Csak akkor ellenőrzi a lemondást, ha a coroutine újraindul, vagy egy másikat co_awaitér el. A probléma az, hogy a megszakításra adott válasz késleltetése potenciálisan túl nagy léptékű.

Egy másik lehetőség tehát, hogy explicit módon lekérdezi a lemondást a coroutine-on belül. Frissítse a fenti példát az alábbi felsorolásban szereplő kóddal. Ebben az új példában az ExplicitCancelationAsync lekéri a winrt::get_cancellation_token függvény által visszaadott objektumot, és rendszeres időközönként ellenőrzi, hogy a korutint törölték-e. Amíg nem szakítják meg, a korutin végtelenül ciklusban fut; amint megszakítják, a ciklus és a függvény is normálisan befejeződik. Az eredmény ugyanaz, mint az előző példában, de itt a kilépés explicit módon történik, és ellenőrzés alatt áll.

IAsyncAction ExplicitCancelationAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };

    while (!cancelation_token())
    {
        std::cout << "ExplicitCancelationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto explicit_cancelation{ ExplicitCancelationAsync() };
    co_await 3s;
    explicit_cancelation.Cancel();
}
...

A winrt::get_cancellation_token függvényre való várakozás egy olyan megszakítási tokent ad vissza, amely ismeri azt a IAsyncActiont, amelyet a korutin az Ön nevében hoz létre. Azon a tokenen a függvényhívó operátort használva lekérdezheti a megszakítás állapotát – lényegében a megszakításra kérdez rá ismételten. Ha valamilyen számításhoz kötött műveletet hajt végre, vagy nagy gyűjteményen keresztüli iterálást végez, akkor ez egy ésszerű módszer.

Lemondási visszahívás regisztrálása

A Windows-futtatókörnyezet megszakítása nem terjed át automatikusan más aszinkron objektumokra. A Windows SDK 10.0.17763.0-s verziójában (Windows 10, 1809-es verzió) azonban regisztrálhat lemondási visszahívást. Ez egy megelőző hook, amelyen keresztül a megszakítás továbbterjeszthető, és lehetővé teszi a meglévő konkurenciakezelő könyvtárakkal való integrációt.

Ebben a következő kód példában a NestedCoroutineAsync végzi a munkát, de nincs benne speciális lemondási logika. CancelationPropagatorAsync lényegében a beágyazott korutin köré épített burkoló; a burkoló előzetesen továbbítja a megszakítást.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction NestedCoroutineAsync()
{
    while (true)
    {
        std::cout << "NestedCoroutineAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction CancelationPropagatorAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };
    auto nested_coroutine{ NestedCoroutineAsync() };

    cancelation_token.callback([=]
    {
        nested_coroutine.Cancel();
    });

    co_await nested_coroutine;
}

IAsyncAction MainCoroutineAsync()
{
    auto cancelation_propagator{ CancelationPropagatorAsync() };
    co_await 3s;
    cancelation_propagator.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

CancelationPropagatorAsync regisztrál egy lambda függvényt a saját megszakítási visszahívásához, majd felfüggeszti a végrehajtást, amíg a beágyazott művelet be nem fejeződik. A CancelPropagatorAsync lemondása esetén a lemondást a beágyazott koroutinra propagálja. Nem kell lekérdezni a lemondást; és a lemondást sem tiltja határozatlan időre. Ez a mechanizmus elég rugalmas ahhoz, hogy egy olyan koroutin- vagy egyidejűségi könyvtárral együttműködjön, amely semmit sem tud a C++/WinRT-ről.

Jelentéskészítési folyamat

Ha a coroutine az IAsyncActionWithProgress vagy az IAsyncOperationWithProgress függvényt adja vissza, akkor lekérheti a winrt::get_progress_token függvény által visszaadott objektumot, és ezzel jelentést készíthet az előrehaladásról egy folyamatkezelőnek. Íme egy példa kódra.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncOperationWithProgress<double, double> CalcPiTo5DPs()
{
    auto progress{ co_await winrt::get_progress_token() };

    co_await 1s;
    double pi_so_far{ 3.1 };
    progress.set_result(pi_so_far);
    progress(0.2);

    co_await 1s;
    pi_so_far += 4.e-2;
    progress.set_result(pi_so_far);
    progress(0.4);

    co_await 1s;
    pi_so_far += 1.e-3;
    progress.set_result(pi_so_far);
    progress(0.6);

    co_await 1s;
    pi_so_far += 5.e-4;
    progress.set_result(pi_so_far);
    progress(0.8);

    co_await 1s;
    pi_so_far += 9.e-5;
    progress.set_result(pi_so_far);
    progress(1.0);

    co_return pi_so_far;
}

IAsyncAction DoMath()
{
    auto async_op_with_progress{ CalcPiTo5DPs() };
    async_op_with_progress.Progress([](auto const& sender, double progress)
    {
        std::wcout << L"CalcPiTo5DPs() reports progress: " << progress << L". "
                   << L"Value so far: " << sender.GetResults() << std::endl;
    });
    double pi{ co_await async_op_with_progress };
    std::wcout << L"CalcPiTo5DPs() is complete !" << std::endl;
    std::wcout << L"Pi is approx.: " << pi << std::endl;
}

int main()
{
    winrt::init_apartment();
    DoMath().get();
}

Az előrehaladás jelentéséhez hívja meg az előrehaladási tokent úgy, hogy argumentumként az előrehaladás értékét adja meg. Ha ideiglenes eredményt szeretne beállítani, használja a set_result() metódust a folyamatjelző tokenen.

Note

Az ideiglenes eredmények jelentéséhez a C++/WinRT 2.0.210309.3-s vagy újabb verziója szükséges.

A fenti példa úgy dönt, hogy minden előrehaladási jelentéshez ideiglenes eredményt ad. Bármikor jelentést készíthet az ideiglenes eredményekről, ha egyáltalán. Nem kell egy előrehaladási jelentéssel összekapcsolni.

Note

Nem helyes, ha egy aszinkron művelethez vagy művelethez egynél több befejezéskezelőt implementál. A befejezési eseményhez vagy egyetlen delegált tartozhat, vagy co_await delegálhatja. Ha mindkettő van, akkor a második sikertelen lesz. Az alábbi kétféle befejezéskezelő közül bármelyik használható, de ugyanahhoz az aszinkron objektumhoz nem használható mindkettő.

auto async_op_with_progress{ CalcPiTo5DPs() };
async_op_with_progress.Completed([](auto const& sender, AsyncStatus /* status */)
{
    double pi{ sender.GetResults() };
});
auto async_op_with_progress{ CalcPiTo5DPs() };
double pi{ co_await async_op_with_progress };

A befejezési kezelőkkel kapcsolatos további információkért tekintse meg az aszinkron műveletek és műveletek delegálási típusait.

Tűz és felejtés

Néha előfordul, hogy van egy feladat, amelyet egyidejűleg más munkával is el lehet végezni, és nem kell megvárnia, amíg a tevékenység befejeződik (más munka nem függ tőle), és nem is kell ahhoz, hogy értéket adjon vissza. Ebben az esetben elindíthatja a feladatot, és elfelejtheti. Ezt úgy teheti meg, hogy ír egy olyan korutint, amelynek visszatérési típusa winrt::fire_and_forget (a Windows-futtatókörnyezet aszinkron művelettípusai közül valamelyik vagy a concurrency::task helyett).

// main.cpp
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace std::chrono_literals;

winrt::fire_and_forget CompleteInFiveSeconds()
{
    co_await 5s;
}

int main()
{
    winrt::init_apartment();
    CompleteInFiveSeconds();
    // Do other work here.
}

winrt::fire_and_forget az eseménykezelő visszatérési típusaként is hasznos, ha aszinkron műveleteket kell végrehajtania benne. Íme egy példa (lásd a C++/WinRT erős és gyenge hivatkozásait is).

winrt::fire_and_forget MyClass::MyMediaBinder_OnBinding(MediaBinder const&, MediaBindingEventArgs args)
{
    auto lifetime{ get_strong() }; // Prevent *this* from prematurely being destructed.
    auto ensure_completion{ unique_deferral(args.GetDeferral()) }; // Take a deferral, and ensure that we complete it.

    auto file{ co_await StorageFile::GetFileFromApplicationUriAsync(Uri(L"ms-appx:///video_file.mp4")) };
    args.SetStorageFile(file);

    // The destructor of unique_deferral completes the deferral here.
}

Az első argumentum (a feladó) név nélkül marad, mert soha nem használjuk. Ezért nyugodtan meghagyhatjuk referenciaként. De figyelje meg, hogy az args érték szerint kerül átadásra. Lásd a fenti paraméterátadási szakaszt .

Kernelfogópontra vár

A C++/WinRT biztosít egy winrt::resume_on_signal függvényt, amellyel addig függesztheti fel a végrehajtást, amíg egy kernelesemény jelzést nem kap. Ön felel azért, hogy a leíró érvényes maradjon addig, amíg a co_await resume_on_signal(h) vissza nem tér. resume_on_signal maga nem tudja megtenni, mert előfordulhat, hogy már a resume_on_signal megkezdése előtt elvesztette a fogópontot, mint ebben az első példában.

IAsyncAction Async(HANDLE event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle is not valid here.
}

A beérkező HANDLE csak addig érvényes, amíg a függvény vissza nem tér, és ez a függvény (amely egy korutin) az első felfüggesztési pontnál tér vissza (ebben az esetben az első co_await esetén). Miközben a DoWorkAsync függvényre várakozik, a vezérlés visszatér a hívóhoz, a hívó veremkeret kikerül a hatókörből, és többé nem lehet tudni, hogy a kezelőazonosító érvényes lesz-e, amikor a korutin folytatódik.

Technikailag a korutin a paramétereit érték szerint kapja meg, ahogyan annak lennie kell (lásd fentebb: Paraméterátadás). Ebben az esetben azonban egy lépéssel tovább kell mennünk, hogy az útmutatás szellemét követjük (nem csak a betűt). Át kell adnunk egy erős referenciát (más szóval a tulajdonjogot) a fogantyúval együtt. Megmutatjuk, hogy hogyan.

IAsyncAction Async(winrt::handle event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle *is* valid here.
}

A winrt::handle érték szerinti átadása tulajdonosi szemantikát biztosít, ami garantálja, hogy a kernel-leíró a korutin teljes élettartama alatt érvényes marad.

Így hívhatja ezt a koroutint.

namespace
{
    winrt::handle duplicate(winrt::handle const& other, DWORD access)
    {
        winrt::handle result;
        if (other)
        {
            winrt::check_bool(::DuplicateHandle(::GetCurrentProcess(),
		        other.get(), ::GetCurrentProcess(), result.put(), access, FALSE, 0));
        }
        return result;
    }

    winrt::handle make_manual_reset_event(bool initialState = false)
    {
        winrt::handle event{ ::CreateEvent(nullptr, true, initialState, nullptr) };
        winrt::check_bool(static_cast<bool>(event));
        return event;
    }
}

IAsyncAction SampleCaller()
{
    handle event{ make_manual_reset_event() };
    auto async{ Async(duplicate(event)) };

    ::SetEvent(event.get());
    event.close(); // Our handle is closed, but Async still has a valid handle.

    co_await async; // Will wake up when *event* is signaled.
}

Megadhat egy időtúllépési értéket a resume_on_signal számára, ahogy az alábbi példa mutatja.

winrt::handle event = ...

if (co_await winrt::resume_on_signal(event.get(), std::literals::2s))
{
    puts("signaled");
}
else
{
    puts("timed out");
}

Aszinkron időtúllépések, egyszerűen

A C++/WinRT nagymértékben épít a C++ korutinokra. A konkurens kód írására gyakorolt hatásuk forradalmi. Ez a szakasz azokat az eseteket ismerteti, amikor az aszinkron adatok nem fontosak, és csak az eredményt szeretné elérni. Ezért a C++/WinRT IAsyncAction Windows-futtatókörnyezet aszinkron műveleti felületének get függvénye az std::future függvényhez hasonlóan működik.

using namespace winrt::Windows::Foundation;
int main()
{
    IAsyncAction async = ...
    async.get();
    puts("Done!");
}

A get függvény határozatlan ideig blokkol, amíg az aszinkron objektum be nem fejeződik. Az aszinkron objektumok általában nagyon rövid élettartamúak, ezért gyakran csak erre van szükség.

Vannak azonban olyan esetek, amikor ez nem elegendő, és el kell hagynia a várakozást egy kis idő eltelte után. A kód írása mindig is lehetséges volt a Windows-futtatókörnyezet által biztosított építőelemeknek köszönhetően. Most azonban a C++/WinRT sokkal egyszerűbbé teszi a wait_for függvény biztosításával. Az IAsyncAction-en is implementálva van, és ismét hasonló az std::future által biztosítotthoz.

using namespace std::chrono_literals;
int main()
{
    IAsyncAction async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        puts("done");
    }
}

Note

wait_forstd::chrono::d urationt használ a felületen, de az std::chrono::d uration által biztosítottnál kisebb tartományra korlátozódik (körülbelül 49,7 nap).

A következő példában szereplő wait_for körülbelül öt másodpercig vár, majd ellenőrzi a befejezést. Ha az összehasonlítás kedvező, akkor tudja, hogy az aszinkron objektum sikeresen befejeződött, és elkészült. Ha valamilyen eredményre vár, ezt egyszerűen követheti a GetResults metódus meghívásával az eredmény lekéréséhez.

Note

wait_for és get egymást kizárják (nem hívhatja meg mindkettőt). Mindegyik pincérnek számít, és Windows-futtatókörnyezet aszinkron műveletek/műveletek csak egyetlen pincért támogatnak.

int main()
{
    IAsyncOperation<int> async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        printf("result %d\n", async.GetResults());
    }
}

Mivel az aszinkron objektum addig befejeződött, a GetResults metódus azonnal, további várakozás nélkül visszaadja az eredményt. Amint látható, wait_for az aszinkron objektum állapotát adja vissza. Így részletesebb vezérlésre is használhatja, mint ez.

switch (async.wait_for(5s))
{
case AsyncStatus::Completed:
    printf("result %d\n", async.GetResults());
    break;
case AsyncStatus::Canceled:
    puts("canceled");
    break;
case AsyncStatus::Error:
    puts("failed");
    break;
case AsyncStatus::Started:
    puts("still running");
    break;
}
  • Ne feledje, hogy az AsyncStatus::Completed azt jelenti, hogy az aszinkron objektum sikeresen befejeződött, és meghívhatja a GetResults metódust az eredmények lekéréséhez.
  • AsyncStatus::A megszakított érték azt jelenti, hogy az aszinkron objektum megszakadt. A hívó általában lemondást kér, ezért ritkán fordul elő, hogy ezt az állapotot kezelje. A megszakított aszinkron objektumokat általában egyszerűen elvetik. Ha szeretné, meghívhatja a GetResults metódust a lemondási kivétel visszaállításához.
  • AsyncStatus::A hiba azt jelenti, hogy az aszinkron objektum valamilyen módon meghiúsult. Ha szeretné, meghívhatja a GetResults metódust a kivétel visszaállításához.
  • AsyncStatus::Az indítás azt jelenti, hogy az aszinkron objektum továbbra is fut. A Windows-futtatókörnyezet aszinkron mintája nem teszi lehetővé sem a többszöri várakozást, sem a több várakozó felet. Ez azt jelenti, hogy ciklusban nem hívhatja meg a(z) wait_for függvényt. Ha a várakozás ténylegesen időtúllépéssel járt, akkor csak néhány lehetősége marad. Az objektumot felhagyhatja, vagy lekérdezheti annak állapotát, mielőtt meghívná a GetResults metódust, hogy bármilyen eredményt lekérjen. De a legjobb, ha ezen a ponton elveti az objektumot.

Egy másik minta az, hogy csak az Első lépések lehetőséget ellenőrzi, és hagyja, hogy a GetResults foglalkozzon a többi esettel.

if (async.wait_for(5s) == AsyncStatus::Started)
{
    puts("timed out");
}
else
{
    // will throw appropriate exception if in canceled or error state
    auto results = async.GetResults();
}

Tömb aszinkron módon történő visszaadása

Az alábbi példa a MIDL 3.0 használatára a következő hibát eredményezi: error MIDL2025: [msg]szintaktikai hiba [context]: a következőt várja: > vagy "," a(z) "[" közelében.

Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();

Ennek az az oka, hogy érvénytelen egy tömb paramétertípus-argumentumként való használata egy paraméteres illesztőhöz. Ezért egy kevésbé nyilvánvaló megoldásra van szükségünk arra, hogy egy tömböt aszinkron módon visszaadjunk egy futásidejű osztály metódusából.

A Tulajdonságérték objektumba bekeretezett tömböt visszaadhatja. A hívókód ezután kicsomagolja. Íme egy kódpélda, amelyet úgy próbálhat ki, hogy hozzáadja a SampleComponent futásidejű osztályt egy Windows-futtatókörnyezet Component (C++/WinRT) projekthez, majd azt felhasználja például egy Blank App, Packaged (WinUI 3 in Desktop) projektben.

// SampleComponent.idl
namespace MyComponentProject
{
    runtimeclass SampleComponent
    {
        Windows.Foundation.IAsyncOperation<IInspectable> RetrieveCollectionAsync();
    };
}

// SampleComponent.h
...
struct SampleComponent : SampleComponentT<SampleComponent>
{
    ...
    Windows::Foundation::IAsyncOperation<Windows::Foundation::IInspectable> RetrieveCollectionAsync()
    {
        co_return Windows::Foundation::PropertyValue::CreateInt32Array({ 99, 101 }); // Box an array into a PropertyValue.
    }
}
...

// SampleCoreApp.cpp
...
MyComponentProject::SampleComponent m_sample_component;
...
auto boxed_array{ co_await m_sample_component.RetrieveCollectionAsync() };
auto property_value{ boxed_array.as<winrt::Windows::Foundation::IPropertyValue>() };
winrt::com_array<int32_t> my_array;
property_value.GetInt32Array(my_array); // Unbox back into an array.
...

Fontos API-k