Megosztás a következőn keresztül:


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.

Munka kiszervezése a Windows-szálkészletre

A koroutin olyan függvény, mint bármely más abban, hogy a hívó le van tiltva, amíg egy függvény vissza nem adja a végrehajtást. Az első lehetőség, hogy egy korutin visszatérjen, az első co_await, co_return, vagy co_yield.

Tehát mielőtt számítási műveletet végez egy koroutinban, vissza kell adnia a végrehajtást a hívónak (más szóval felfüggesztési pontot kell bevezetnie), hogy a hívó ne legyen blokkolva. 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őt a hívónak, majd azonnal folytatja a végrehajtást egy szálkészlet 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ál affinitás figyelembevételével

Ez a forgatókönyv továbbfejleszti az előzőt. 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ál kontextust, amelyben a koroutint eredetileg meghívták. Ehhez példányosítson egy winrt::apartment_context objektumot, végezze el a háttérmunkát, majd az co_await-tel váltson vissza a hívási 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 az előtérszálat a TextBlock-hoz társított diszpécser objektumot átadva határozzuk meg, azáltal, hogy elérjük a Dispatcher tulajdonságát. A winrt::resume_foreground meghívja a CoreDispatcher.RunAsync parancsot azon a diszpécserobjektumon, 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.Dispatcher());

    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 leegyszerűsíti co_await winrt::resume_foreground(someDispatcherObject);-t egyszerűen csak co_await someDispatcherObject;értékre.

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 eltűnhet, és az újrakezdés bármelyik másik szálon megtörténhet (más szóval bármelyik szál meghívhatja az aszinkron művelet Completed metódusát).

Ha co_await a négy Windows Runtime aszinkron művelet típus (IAsyncXxx) bármelyikét használja, akkor a C++/WinRT rögzíti a hívási környezetet, amikor co_await. Ez biztosítja, hogy még mindig ugyanabban a kontextusban legyen, amikor a folytatás újra kezdődik. A C++/WinRT ezt úgy teszi, hogy ellenőrzi, hogy már a hívókörnyezetben van-e, és ha nem, akkor átvált arra. Ha co_awaitelőtt egy egyszálas apartman (STA) szálon volt, akkor ugyanazon a szálon marad; ha co_awaitelőtt egy többszálas apartman (MTA) szálon volt, akkor egyen marad.

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 a Windows Futtatókörnyezet aszinkron művelettípusainak A C++ coroutine nyelvi támogatásához 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álmedence burkolói és/vagy segítői. Ezek ezért a szálmedencén 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 valamilyen más típusú co_await – még a C++/WinRT korutin implementáción belül is –, akkor egy másik könyvtár biztosítja az adaptereket, és fontos megértenie, hogy ezek az adapterek mit tesznek a folytatás és a környezet szempontjából.

A környezeti váltások minimalizálása érdekében használhatja a jelen 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 olyan eseménykezelő körvonalát mutatjuk be, amely meghív egy Windows Runtime API-t egy kép betöltéséhez, egy háttérszálra 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->Dispatcher());

    // 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 Runtime API-t hívunk meg a winrt::resume_background hívás előtt, annál több szükségtelen kontextusváltást végzünk. A megoldás az, hogy még nem hív 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->Dispatcher());

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

    // Display the image in the UI.
}

Ha valami összetettebbet szeretne tenni, akkor megírhatja a saját várakozási adaptereit. Ha például azt szeretné, hogy egy co_await ugyanazon a szálon folytatódjon, amelyen az aszinkron művelet befejeződik (tehát nincs kontextusváltás), akkor az alábbihoz hasonló await adaptereket írhat.

Megjegyzés:

Az alábbi példakód csak oktatási célokra szolgál; ez azért van, hogy elkezdje 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 és tesztelje saját await adapter struktúrá(já)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 ön alapértelmezés szerint ésszerű viselkedést kapjon, ehhez hasonlóan.

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

A no_switch váró adapterek használatához módosítsa annak a kifejezésnek a típusát co_await, az IAsyncXxx-rőlno_switch-ra, ilyen módon.

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

Ezután az IAsyncXxxawait_xxx függvény helyett a C++ fordító az no_switchmegfelelő függvényeket keresi.

A winrt részletesebb bemutatása::resume_foreground

A C++/WinRT 2.0esetében 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 visszafejtésére és az újrasorba állításra, ami fontos a rendszer stabilitása szempontjából, különösen az alacsony szintű rendszerkódok esetében. A fenti, szál-affinitást szem előtt tartó szakasz utolsó kódlistázása egy háttérszálon végrehajtott összetett számítást szemléltet, majd a felhasználói felület frissítéséhez átvált a megfelelő szálra.

Í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 visszatekerése alapvető fontosságú lehet.

A winrt::resume_foreground függvény kezdetben csak a CoreDispatchert (egy CoreWindow-hoz kötött) támogatta, amely a Windows 10 előtt lett bevezetve. Azóta bevezettünk egy rugalmasabb és hatékonyabb diszpécsert: 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 CreateDispatcherQueueController függvényt ezután meghívják a sorkezelő létrehozására, mielőtt meghívnak egy coroutine-t a vezérlő tulajdonában lévő üzenetváró sorral. Belépünk a hagyományos üzenetkezelő ciklusba, ahol a coroutine futása természetesen ezen a szálon folytatódik. 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;
 
    ...
}

Megjegyzés:

Ahogy fentebb látható, mindenképpen adja meg a vetítés fejlécét annak a névtérnek, amelyben a co_await-típust használja. Windows ::UI::Core::CoreDispatcher, Windows::System::DispatcherQueue vagy Microsoft::UI::Dispatching::DispatcherQueue.

Vagy ebben az esetben észleli az üzenetsor leállítását, és kezeli azt elegánsan.

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

A co_await kifejezés trueeredményt ad, ami azt jelzi, hogy az újrakezdés a diszpécserszálon történik. Más szóval ez a sorba állítás sikeres volt. Ezzel szemben a rendszer false ad vissza annak jelzésére, hogy a végrehajtás a hívó szálon marad, mert az üzenetsor vezérlője leáll, és már nem szolgálja ki az üzenetsor-kérelmeket.

Tehát nagy ereje van a kezében, amikor a C++/WinRT-t a korutinokkal kombinálja; és különösen, amikor egy hagyományos Petzold-stílusú asztali alkalmazásfejlesztést hajt végre.

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

A 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 Windows::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 van, akkor rövidzárlatot okoz; és ha nincs, akkor a megszokott módon felfüggesztésre kerül.

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ásik co_awaitér el. A probléma az egyik potenciálisan túl durva késés a lemondásra való válaszadás során.

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 korutin megszakadt-e. Mindaddig, amíg nincs törölve, a korutin végtelenül hurokban fut; ha megszakítják, a hurok és a függvény a szokásos módon kilép. 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-ra való várakozás egy törlési tokent ad vissza azzal az IAsyncAction-nel kapcsolatos információval, amelyet a korrutin az Ön nevében készít. A token függvényhívási operátorával lekérdezheti a megszakítási állapotot – lényegében ellenőrzést végezhet a megszakítás miatt. 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 Runtime 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 előzetes horog, amellyel a lemondás propagálható, és lehetővé teszi a meglévő egyidejűségi kódtárakkal való integrálást.

Ebben a következő kód példában a NestedCoroutineAsync végzi a munkát, de nincs benne speciális lemondási logika. A CancellationPropagatorAsync lényegében egy burkoló a beágyazott korutin körül; a burkoló előre 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 törlési visszahívásához, majd felfüggeszti magát, amíg a beágyazott munka befejeződéséig vár. Amikor vagy ha a CancellationPropagatorAsync törlése történik, a törlést a beágyazott koroutinra propagálja. Nem kell kérdezni a lemondást; és a lemondás sincs végleg blokkolva. 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 használja az előrehaladási tokent a progressz értékével mint argumentummal. Ideiglenes eredmény beállításához használja a set_result() módszert a folyamat tokenen.

Megjegyzés:

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.

Megjegyzés:

Nem helyes, ha több befejezéskezelőt implementál egy aszinkron művelethez vagy művelethez. A befejezett eseményhez lehet egy meghatalmazott, vagy megteheti a co_await műveletet. Ha mindkettő van, akkor a második sikertelen lesz. A következő két típusú befejezési kezelő közül az egyik megfelelő; de nem mindkettő ugyanarra az aszinkron objektumra.

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 koroutint, amelynek visszatérési típusa winrt::fire_and_forget (egy Windows Runtime aszinkron művelettípus vagy 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 C++/WinRT erős és gyenge hivatkozásait ).

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 hagyhatjuk referenciának. 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 egy winrt::resume_on_signal függvényt biztosít, amellyel felfüggesztheti a működést, amíg egy rendszermag eseményre jelzést kap. Ön felelős azért, hogy a fogantyú érvényes maradjon, 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 bejövő HANDLE csak a függvény visszatéréséig érvényes, és ez a függvény (amely egy korutin) az első felfüggesztési ponton tér vissza (ebben az esetben az első co_await). Amíg a DoWorkAsync-ra vár, a vezérlő visszatért a hívóhoz, a hívókeret kiment a hatókörből, és már nem tudja, hogy a leíró érvényes lesz-e a koroutine folytatásakor.

Technikailag a koroutin érték szerint kapja meg a paramétereit, ahogy kell (lásd a fenti paraméterátadást ). 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 birtoklási szemantikát biztosít, amely biztosítja, hogy a kernelfogópont érvényes maradjon a koroutin teljes élettartamára.

Í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.
}

Az resume_on_signal függvényhez időtúllépési értéket is megadhat, ahogy ez a példában látható.

winrt::handle event = ...

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

Az aszinkron időtúllépések kezelése egyszerűvé válik

A C++/WinRT erőteljesen összpontosít a C++ korutinokra. Az egyidejű kód írására gyakorolt hatásuk átalakító. 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 Runtime 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 korlátlanul blokkol, amíg az aszinkron objektum befejező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");
    }
}

Megjegyzés:

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.

Megjegyzés:

wait_for és get kölcsönösen kizáróak (nem hívhatja meg őket egyszerre). Mindegyik pincérnek számít, és a Windows Futtatókörnyezet aszinkron műveletei/műveletei 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é a többszörös várakozást, illetve várakozókat sem. Ez azt jelenti, hogy nem lehet meghívni a wait_for függvényt egy ciklusban. Ha a várakozás lejárt, akkor Önnek marad néhány lehetősége. 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();
}

Aszinkron módon tömb visszaadása

Az alábbi példa a MIDL 3.0 alkalmazására , amely MIDL2025: [msg] szintaxishibát okoz [kontekstus]: várt > 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ó módszerre van szükségünk ahhoz, hogy a tömböket aszinkron módon vissza lehessen adni egy futtatókörnyezeti 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 azt. Íme egy példa kódra, amelyet kipróbálhat, ha hozzáadja a SampleComponent futtatókörnyezeti osztályt egy Windows Runtime Component (C+++/WinRT) projekthez, majd ezt egy Core App (C+++/WinRT) projektből használja fel.

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