Geavanceerde gelijktijdigheid en asynchroonheid met C++/WinRT

Dit onderwerp beschrijft geavanceerde scenario's voor gelijktijdigheid en asynchronie in C++/WinRT.

Lees voor een inleiding tot dit onderwerp eerst gelijktijdigheid en asynchrone bewerkingen.

Werk overbrengen naar de Windows-threadpool

Een coroutine is net als elke andere functie: de aanroeper wordt geblokkeerd totdat de functie de besturing eraan teruggeeft. En, de eerste mogelijkheid voor een coroutine om terug te keren is de eerste co_await, co_return of co_yield.

Dus voordat u rekengebonden werk uitvoert in een coroutine, moet u de uitvoering teruggeven aan de aanroeper (met andere woorden: een onderbrekingspunt introduceren), zodat de aanroeper niet wordt geblokkeerd. Als u dat nog niet doet door een andere bewerking co_await uit te voeren, kunt u co_await de functie winrt::resume_background aanroepen. Dat geeft de besturing terug aan de aanroeper en vervolgens wordt de uitvoering onmiddellijk hervat op een threadpoolthread.

De threadpool die in de implementatie wordt gebruikt, is de Windows threadpool op laag niveau, dus deze is optimaal efficiënt.

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;
}

Programmeren rekening houdend met thread-affiniteit

In dit scenario wordt het vorige scenario uitgebreid. U kunt wat werk offloaden naar de threadpool, maar u wilt de voortgang weergeven in de gebruikersinterface (UI).

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

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

Met de bovenstaande code wordt een winrt::hresult_wrong_thread uitzondering gegenereerd, omdat een TextBlock moet worden bijgewerkt vanuit de thread die deze heeft gemaakt. Dit is de UI-thread. Een oplossing is het vastleggen van de threadcontext waarin onze coroutine oorspronkelijk werd aangeroepen. U doet dit door een winrt::apartment_context-object te instantiëren, achtergrondwerk uit te voeren en vervolgens co_await de apartment_context om terug te gaan naar de aanroepende context.

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

Zolang de bovenstaande coroutine wordt aangeroepen vanuit de UI-thread die de TextBlock heeft gemaakt, werkt deze techniek. Er zijn veel gevallen in uw app waar u zeker van bent.

Voor een algemenere oplossing voor het bijwerken van de gebruikersinterface, die situaties dekt waarin u niet zeker weet wat de aanroepende thread is, kunt u co_await de functie winrt::resume_foreground gebruiken om over te schakelen naar een specifieke foreground-thread. In het onderstaande codevoorbeeld geven we de voorgrondthread op door de dispatcherwachtrij door te geven die is gekoppeld aan de TextBlock (door toegang te krijgen tot de eigenschap DispatcherQueue ). De implementatie van winrt::resume_foreground roept DispatcherQueue.TryEnqueue aan op dat dispatcherwachtrij-object om het werk uit te voeren dat erna in de coroutine volgt.

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

De functie winrt::resume_foreground heeft een optionele prioriteitsparameter. Als u deze parameter gebruikt, is het bovenstaande patroon geschikt. Zo niet, dan kunt u ervoor kiezen om te vereenvoudigen co_await winrt::resume_foreground(someDispatcherObject); in alleen co_await someDispatcherObject;.

Uitvoeringscontexten, hervatten en overschakelen in een coroutine

In grote lijnen geldt dat na een suspensiepunt in een coroutine de oorspronkelijke uitvoerende thread kan verdwijnen en de hervatting op om het even welke thread kan plaatsvinden (met andere woorden: elke thread kan de methode Completed van de asynchrone bewerking aanroepen).

Maar als u co_await een van de vier typen asynchrone bewerkingen van Windows Runtime (IAsyncXxx), legt C++/WinRT de aanroepcontext vast op het moment dat u co_await. En het zorgt ervoor dat u zich nog steeds in die context bevindt wanneer de voortzetting wordt hervat. C++/WinRT doet dit door te controleren of u zich al in de aanroepende context bevindt en, als dat niet het geval is, naar deze context over te schakelen. Als u zich vóór co_await op een STA-thread (single-threaded apartment) bevond, dan bevindt u zich daarna nog steeds op dezelfde thread; als u zich vóór co_await op een MTA-thread (multi-threaded apartment) bevond, dan bevindt u zich daarna op een MTA-thread.

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

De reden waarom u op dit gedrag kunt vertrouwen, is omdat C++/WinRT code biedt om deze Windows Runtime asynchrone bewerkingstypen aan te passen aan de C++ coroutine taalondersteuning (deze stukjes code worden wachtadapters genoemd). De resterende awaitbare typen in C++/WinRT zijn simpelweg wrappers voor de threadpool en/of helpers; dus ze worden op de threadpool voltooid.

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;
}

Als u co_await een ander type gebruikt — zelfs binnen een coroutine-implementatie van C++/WinRT — worden de adapters door een andere bibliotheek geleverd, en moet u begrijpen wat die adapters doen met betrekking tot hervatting en contexten.

Om het wisselen van context tot een minimum te beperken, kunt u enkele van de technieken gebruiken die we al in dit onderwerp hebben gezien. Laten we eens wat illustraties bekijken om dat te doen. In dit volgende pseudocodevoorbeeld laten we het overzicht zien van een gebeurtenishandler die een Windows Runtime API aanroept om een afbeelding te laden, neer te zetten op een achtergrondthread om die afbeelding te verwerken en vervolgens terugkeert naar de UI-thread om de afbeelding weer te geven in de gebruikersinterface.

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

Voor dit scenario is er een beetje inefficiëntie rond de aanroep naar StorageFile::OpenAsync. Er is een noodzakelijke contextswitch naar een achtergrondthread (zodat de handler de uitvoering naar de aanroeper kan retourneren), na hervatting waarna C++/WinRT de context van de UI-thread herstelt. Maar in dit geval is het niet nodig om in de ui-thread te zijn totdat de gebruikersinterface wordt bijgewerkt. Hoe meer Windows Runtime API's we aanroepen vóór onze aanroep van winrt::resume_background, hoe meer onnodige contextwisselingen heen en weer we oplopen. De oplossing is vóór die tijd geen Windows Runtime API's aanroepen. Verplaats ze allemaal na winrt::resume_background.

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

Als u iets geavanceerder wilt doen, kunt u uw eigen await-adapters schrijven. Als u bijvoorbeeld wilt dat een co_await wordt hervat op dezelfde thread waarop de asynchrone actie wordt voltooid (dus zonder contextwisseling), kunt u beginnen met het schrijven van await-adapters zoals hieronder weergegeven.

Note

Het onderstaande codevoorbeeld is alleen bedoeld voor leerdoeleinden; het is bedoeld om u op weg te helpen bij het begrijpen van hoe await-adapters werken. Als u deze techniek in uw eigen codebasis wilt gebruiken, raden we u aan uw eigen await adapter-struct(en) te ontwikkelen en te testen. U kunt bijvoorbeeld complete_on_any, complete_on_current en complete_on(dispatcher) schrijven. Overweeg ook sjablonen te maken die het type IAsyncXxx als sjabloonparameter gebruiken.

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;
};

Om te begrijpen hoe u de no_switch await-adapters gebruikt, moet u eerst weten dat wanneer de C++-compiler een co_await-expressie tegenkomt, deze zoekt naar functies met de naam await_ready, await_suspend en await_resume. De C++/WinRT-bibliotheek biedt deze functies, zodat u standaard redelijk gedrag krijgt, zoals dit.

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

Om de no_switch await-adapters te gebruiken, hoeft u alleen maar het type van die co_await-expressie te wijzigen van IAsyncXxx naar no_switch, zoals hier.

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

In plaats van te zoeken naar de drie await_xxx functies die overeenkomen met IAsyncXxx, zoekt de C++-compiler naar functies die overeenkomen met no_switch.

Meer informatie over winrt::resume_foreground

Vanaf C++/WinRT 2.0 schort de functie winrt::resume_foreground de uitvoering op, zelfs als deze wordt aangeroepen vanuit de dispatcherthread (in eerdere versies kon dit in sommige scenario's deadlocks veroorzaken, omdat de uitvoering alleen werd opgeschort als deze nog niet op de dispatcherthread werd uitgevoerd).

Het huidige gedrag betekent dat u ervan uit kunt gaan dat stack-unwinding en het opnieuw in de wachtrij plaatsen plaatsvinden; en dat is belangrijk voor de systeemstabiliteit, vooral in laag-niveau-systeemcode. De laatste codevermelding in de sectie Programmeren met threadaffiniteit in gedachten, hierboven, illustreert het uitvoeren van een complexe berekening op een achtergrondthread en vervolgens overschakelen naar de juiste UI-thread om de gebruikersinterface (UI) bij te werken.

Zo ziet winrt::resume_foreground er intern uit.

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{ ... };
};

Dit huidige, versus vorige gedrag is vergelijkbaar met het verschil tussen PostMessage en SendMessage in win32-toepassingsontwikkeling. PostMessage plaatst het werk in de wachtrij en wikkelt de stack af zonder te wachten tot het werk is voltooid. Het afwikkelen van de stack kan essentieel zijn.

De functie winrt::resume_foreground ondersteunde oorspronkelijk de CoreDispatcher (gekoppeld aan een CoreWindow), die werd geïntroduceerd vóór Windows 10. Gebruik in WinUI 3 en Windows App SDK apps de DispatcherQueue in plaats daarvan. U kunt een DispatcherQueue maken voor uw eigen doeleinden. Houd rekening met deze eenvoudige consoletoepassing.

using namespace Windows::System;

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

In het bovenstaande voorbeeld wordt op een privéthread een wachtrij aangemaakt (ondergebracht in een controller), waarna de controller aan de coroutine wordt doorgegeven. De coroutine kan de wachtrij gebruiken om te wachten (onderbreken en hervatten) op de privéthread. Een ander veelvoorkomend gebruik van DispatcherQueue is het maken van een wachtrij op de huidige UI-thread voor een traditionele bureaublad- of Win32-app.

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 };
}

Dit laat zien hoe u Win32-functies kunt aanroepen en opnemen in uw C++/WinRT-projecten door simpelweg de Win32-stijl CreateDispatcherQueueController-functie aan te roepen om de controller te maken en vervolgens het eigendom van de resulterende wachtrijcontroller over te dragen aan de aanroeper als een WinRT-object. Dit is ook precies hoe u efficiënte en naadloze wachtrijen op uw bestaande Petzold-stijl Win32-bureaubladtoepassing kunt ondersteunen.

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);
    }
}

Hierboven begint de eenvoudige hoofdfunctie met het maken van een venster. U kunt zich voorstellen dat dit een vensterklasse registreert en CreateWindow aanroept om het bureaubladvenster op het hoogste niveau te maken. De functie CreateDispatcherQueueController wordt vervolgens aangeroepen om de wachtrijcontroller te maken voordat u een coroutine aanroept met de dispatcherwachtrij die eigendom is van deze controller. Een traditionele berichtpomp wordt vervolgens ingevoerd waar hervatting van de coroutine natuurlijk op deze draad voorkomt. Als u dat hebt gedaan, kunt u terugkeren naar de elegante wereld van coroutines voor uw asynchrone of berichtengebaseerde werkstroom in uw toepassing.

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

De aanroep van winrt::resume_foreground zal altijd in de wachtrij plaatsen en vervolgens de stack afwikkelen. U kunt eventueel ook de hervattingsprioriteit instellen.

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

Of gebruik de standaardvolgorde voor wachtrijen.

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

Note

Zoals hierboven te zien is, moet u de projectieheader opnemen voor de naamruimte van het type waarvoor u co_await gebruikt. Bijvoorbeeld Windows::System::DispatcherQueue of Microsoft::UI::Dispatching::DispatcherQueue.

Of, in dit geval, het afsluiten van een wachtrij detecteren en daar netjes mee omgaan.

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

De co_await expressie retourneert true, waarmee wordt aangegeven dat hervatting plaatsvindt op de dispatcherthread. Met andere woorden, die wachtrij is geslaagd. Daarentegen wordt false geretourneerd om aan te geven dat de uitvoering op de aanroepende thread blijft, omdat de wachtrijcontroller wordt afgesloten en geen wachtrijverzoeken meer afhandelt.

U hebt dus veel mogelijkheden tot uw beschikking wanneer u C++/WinRT combineert met coroutines, en vooral wanneer u desktoptoepassingen ontwikkelt in de klassieke Petzold-stijl.

Een asynchrone bewerking annuleren en callbacks annuleren

Met de functies van de Windows Runtime voor asynchrone programmering kunt u een asynchrone actie of bewerking tijdens de vlucht annuleren. Hier volgt een voorbeeld waarin StorageFolder::GetFilesAsync wordt aangeroepen om een mogelijk grote verzameling bestanden op te halen en het resulterende asynchrone bewerkingsobject opslaat in een gegevenslid. De gebruiker heeft de mogelijkheid om de bewerking te annuleren.

// 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;
};
...

Laten we beginnen met een eenvoudig voorbeeld voor de implementatiezijde van annulering.

// 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();
}

Als u het bovenstaande voorbeeld uitvoert, ziet u dat ImplicieteCancelationAsync gedurende drie seconden één bericht per seconde afdrukt, waarna deze automatisch wordt beëindigd als gevolg van het annuleren. Dit werkt omdat bij het tegenkomen van een co_await expressie een coroutine controleert of deze is geannuleerd. Als dat het geval is, wordt het direct afgebroken; en als dat niet zo is, wordt het zoals gebruikelijk opgeschort.

Het annuleren kan natuurlijk gebeuren terwijl de coroutine is opgeschort. Alleen wanneer de coroutine wordt hervat of opnieuw een co_await bereikt, controleert deze op annulering. Het probleem is dat de reactievertraging op annulering mogelijk te grofkorrelig is.

Een andere optie is om expliciet te peilen naar annulering vanuit uw coroutine. Werk het bovenstaande voorbeeld bij met de code in de onderstaande vermelding. In dit nieuwe voorbeeld haalt ExplicitCancelationAsync het object op dat wordt geretourneerd door de functie winrt::get_cancellation_token en wordt dit gebruikt om periodiek te controleren of de coroutine is geannuleerd. Zolang de coroutine niet wordt geannuleerd, blijft deze oneindig doorlopen; zodra die wordt geannuleerd, worden de lus en de functie normaal beëindigd. Het resultaat is hetzelfde als in het vorige voorbeeld, maar hier gebeurt het beëindigen expliciet en op een gecontroleerde manier.

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();
}
...

Door te wachten op winrt::get_cancellation_token wordt een annuleringstoken opgehaald dat op de hoogte is van de IAsyncAction die de coroutine namens u oplevert. U kunt de operator voor functieoproep op dat token gebruiken om een query uit te voeren op de annuleringsstatus. In wezen wordt gecontroleerd op annulering. Als u een berekeningsgebonden bewerking uitvoert of een grote verzameling herhaalt, is dit een redelijke techniek.

Een annuleringsaanroep registreren

De annulering van de Windows Runtime wordt niet automatisch doorgegeven aan andere asynchrone objecten. Maar, geïntroduceerd in versie 10.0.17763.0 (Windows 10, versie 1809) van de Windows SDK, kunt u een annuleringsaanroep registreren. Dit is een preventieve hook waarmee annulering kan worden doorgegeven en maakt het mogelijk om te integreren met bestaande gelijktijdigheidsbibliotheken.

In dit volgende codevoorbeeld werkt NestedCoroutineAsync , maar heeft er geen speciale annuleringslogica in. CancelationPropagatorAsync is in feite een wrapper op de geneste coroutine; de wrapper stuurt annulering vooraf door.

// 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 registreert een lambdafunctie voor de eigen annuleringscallback en wacht vervolgens (wordt opgeschort) totdat de geneste bewerking is voltooid. Wanneer of als CancellationPropagatorAsync wordt geannuleerd, wordt de annulering doorgegeven aan de geneste coroutine. Het is niet nodig om op annulering te pollen; annulering wordt ook niet voor onbepaalde tijd geblokkeerd. Dit mechanisme is flexibel genoeg om het te gebruiken om te interopen met een coroutine- of gelijktijdigheidsbibliotheek die niets van C++/WinRT kent.

Voortgang rapporteren

Als uw coroutine IAsyncActionWithProgress of IAsyncOperationWithProgress retourneert, kunt u het object ophalen dat wordt geretourneerd door de functie winrt::get_progress_token en deze gebruiken om de voortgang terug te rapporteren naar een voortgangshandler. Hier volgt een codevoorbeeld.

// 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();
}

Als u de voortgang wilt rapporteren, roept u het voortgangstoken aan met de voortgangswaarde als argument. Als u een voorlopig resultaat wilt instellen, gebruikt u de set_result() methode voor het voortgangstoken.

Note

Voor het rapporteren van voorlopige resultaten is C++/WinRT versie 2.0.210309.3 of hoger vereist.

In het bovenstaande voorbeeld wordt ervoor gekozen om een voorlopig resultaat in te stellen voor elk voortgangsrapport. U kunt ervoor kiezen om op elk gewenst moment voorlopige resultaten te rapporteren. Het rapport hoeft niet te worden gekoppeld aan een voortgangsrapport.

Note

Het is niet juist om meer dan één voltooiingshandler te implementeren voor een asynchrone actie of bewerking. U kunt één gedelegeerde hebben voor de voltooide gebeurtenis, of u kunt co_await deze. Als u beide hebt, mislukt de tweede. Een van de volgende twee soorten voltooiingshandlers is geschikt; niet beide voor hetzelfde asynchrone object.

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 };

Zie Gemachtigdentypen voor asynchrone acties en bewerkingen voor meer informatie over voltooiingshandlers.

Vuur en vergeet

Soms hebt u een taak die gelijktijdig met ander werk kan worden uitgevoerd, waarbij u niet hoeft te wachten tot die taak is voltooid (er is geen ander werk dat ervan afhankelijk is) en die ook geen waarde hoeft te retourneren. In dat geval kunt u de taak afvuren en vergeten. U kunt dit doen door een coroutine te schrijven waarvan het retourtype winrt::fire_and_forget is (in plaats van een van de Windows Runtime asynchrone bewerkingstypen, of gelijktijdigheid::taak).

// 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 is ook handig als retourtype van uw gebeurtenisafhandelaar wanneer u daarin asynchrone bewerkingen moet uitvoeren. Hier volgt een voorbeeld (zie ook sterke en zwakke verwijzingen in C++/WinRT).

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

Het eerste argument (de afzender) blijft ongenaamd, omdat we het nooit gebruiken. Daarom kunnen we het veilig als referentie behouden. Maar merk op dat args op waarde wordt doorgegeven. Zie de sectie Doorgeven van parameters hierboven.

Wachten op een kernel-handle

C++/WinRT biedt een winrt::resume_on_signal-functie , die u kunt gebruiken om te onderbreken totdat een kernelgebeurtenis wordt gesignaleerd. U bent ervoor verantwoordelijk dat de handle geldig blijft totdat uw co_await resume_on_signal(h) terugkeert. resume_on_signal kan dat zelf niet voor je doen, omdat je de handle mogelijk al bent kwijtgeraakt voordat resume_on_signal begint, zoals in dit eerste voorbeeld.

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

De meegegeven HANDLE is alleen geldig totdat de functie terugkeert, en deze functie (een coroutine) keert terug bij het eerste onderbrekingspunt (in dit geval de eerste co_await). Terwijl u wacht op DoWorkAsync, is de uitvoering teruggekeerd naar de aanroeper, is het aanroepende frame buiten de scope geraakt en weet u niet langer of de handle geldig zal zijn wanneer uw coroutine wordt hervat.

Technisch gezien ontvangt onze coroutine zijn parameters per waarde, zoals het hoort (zie Parameteroverdracht hierboven). Maar in dit geval moeten we een stap verder gaan, zodat we de geest van die richtlijnen volgen (in plaats van alleen de letter). We moeten samen met de handle een sterke referentie doorgeven (met andere woorden: het eigenaarschap). Dit doet u als volgt.

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

Het doorgeven van een winrt::handle by value biedt semantiek van eigendom, waardoor de kernel-handle geldig blijft voor de levensduur van de coroutine.

Zo kunt u die coroutine aanroepen.

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

U kunt een time-outwaarde doorgeven aan resume_on_signal, zoals in dit voorbeeld.

winrt::handle event = ...

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

Asynchrone timeouts eenvoudig gemaakt

C++/WinRT maakt sterk gebruik van C++ coroutines. Hun effect op het schrijven van gelijktijdigheidscode is transformatief. In deze sectie worden gevallen besproken waarin de details van asynchroniciteit niet belangrijk zijn en u alleen het resultaat meteen wilt hebben. Daarom heeft C++/WinRT's implementatie van de IAsyncAction Windows Runtime asynchrone bewerkingsinterface een get-functie, vergelijkbaar met die van std::future.

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

De get-functie blokkeert voor onbepaalde tijd, terwijl het asynchrone object is voltooid. Asynchrone objecten hebben meestal een korte levensduur, dus dit is vaak alles wat u nodig hebt.

Maar er zijn gevallen waarin dat niet voldoende is en u de wachttijd moet verlaten nadat enige tijd is verstreken. Het schrijven van die code is altijd mogelijk, dankzij de bouwstenen van de Windows Runtime. Maar nu maakt C++/WinRT het veel eenvoudiger door de wait_for functie op te geven. Het is ook geïmplementeerd op IAsyncAction, en nogmaals is het vergelijkbaar met die van std::future.

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

Note

wait_for maakt gebruik van std::chrono::d uration op de interface, maar het is beperkt tot een beperkt bereik dat kleiner is dan wat std::chrono::d uration biedt (ongeveer 49,7 dagen).

De wait_for in dit volgende voorbeeld wacht ongeveer vijf seconden en controleert de voltooiing. Als de vergelijking gunstig is, weet u dat het asynchrone object is voltooid en u klaar bent. Als u wacht op een resultaat, kunt u dat gewoon volgen met een aanroep naar de Methode GetResults om het resultaat op te halen.

Note

wait_for en get sluiten elkaar uit (je kunt ze niet allebei aanroepen). Ze tellen elk als een waiter, en Windows Runtime-asynchrone acties/bewerkingen ondersteunen maar één enkele waiter.

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

Omdat het asynchrone object vervolgens is voltooid, retourneert de Methode GetResults het resultaat onmiddellijk, zonder verdere wachttijd. Zoals u ziet, retourneert wait_for de status van het asynchrone object. U kunt het dus gebruiken voor meer verfijnde controle, zoals deze.

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;
}
  • Houd er rekening mee dat AsyncStatus::Completed betekent dat het asynchrone object is voltooid en u de methode GetResults kunt aanroepen om een resultaat op te halen.
  • AsyncStatus::Canceled betekent dat het asynchrone object is geannuleerd. Een annulering wordt doorgaans verzocht door de aanroeper, dus deze status hoeft zelden te worden afgehandeld. Normaal gesproken wordt een geannuleerd asynchroon object gewoon verwijderd. U kunt de methode GetResults aanroepen om de annuleringsuitzondering opnieuw op te werpen als u dat wilt.
  • AsyncStatus::Error betekent dat het asynchrone object op een of andere manier is mislukt. U kunt de Methode GetResults aanroepen om de uitzondering opnieuw te plaatsen als u dat wilt.
  • AsyncStatus::Gestart betekent dat het asynchrone object nog steeds wordt uitgevoerd. Het asynchrone patroon van Windows Runtime staat geen meerdere wachtbewerkingen toe, en ook geen meerdere wachters. Dat betekent dat u wait_for niet in een lus kunt aanroepen. Als de wachttijd feitelijk is verstreken, hebt u nog een paar opties. U kunt het object verlaten of u kunt de status ervan peilen voordat u de GetResults-methode aanroept om een resultaat op te halen. Maar het is het beste om het object op dit moment te negeren.

Een alternatief patroon is om alleen te controleren op Aan de slag en GetResults te laten omgaan met de andere gevallen.

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();
}

Een matrix asynchroon retourneren

Hieronder ziet u een voorbeeld van MIDL 3.0 dat fout produceert MIDL2025: [msg]syntaxisfout [context]: verwacht > of, in de buurt van [".

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

De reden hiervoor is dat het ongeldig is om een matrix als parametertypeargument te gebruiken voor een geparameteriseerde interface. We hebben dus een minder voor de hand liggende manier nodig om asynchroon een array terug te sturen vanuit een methode van een runtimeklasse.

U kunt de matrix retourneren die is geplaatst in een PropertyValue-object . De aanroepende code pakt het vervolgens uit. Hier is een codevoorbeeld dat u kunt proberen door de runtimeklasse SampleComponent toe te voegen aan een Windows Runtime Component (C++/WinRT)-project en die vervolgens te gebruiken in bijvoorbeeld een Blank App, Packaged (WinUI 3 in Desktop)-project.

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

Belangrijke API's