Gelijktijdigheid en asynchrone bewerkingen met C++/WinRT

Belangrijk

In dit onderwerp maakt u kennis met de concepten achter coroutines en co_await, waarvan we aanbevelen dat u die zowel in uw UI als in uw niet-UI-toepassingen gebruikt. Ter vereenvoudiging worden in de meeste codevoorbeelden in dit inleidende onderwerp Windows C++/WinRT-projecten (Console Application) weergegeven. In de latere codevoorbeelden in dit onderwerp worden coroutines gebruikt, maar voor het gemak blijven in de voorbeelden van consoletoepassingen ook de blokkerende functieaanroep get gebruiken vlak voor het afsluiten, zodat de toepassing niet wordt afgesloten voordat de uitvoer volledig is afgedrukt. Dat doet u niet (de blokkerende functie get aanroepen) op een UI-thread. In plaats daarvan gebruikt u de co_await statement. De technieken die u in uw UI-toepassingen gaat gebruiken, worden beschreven in het onderwerp Geavanceerde gelijktijdigheid en asynchroonheid.

In dit inleidende onderwerp ziet u enkele manieren waarop u zowel asynchrone objecten kunt maken als gebruiken Windows Runtime met C++/WinRT. Nadat u dit onderwerp hebt gelezen, met name voor technieken die u in uw UI-toepassingen gaat gebruiken, ziet u ook Geavanceerde gelijktijdigheid en asynchroonheid.

Asynchrone bewerkingen en Windows Runtime 'Async'-functies

Elke Windows Runtime-API die meer dan 50 milliseconden in beslag kan nemen, wordt geïmplementeerd als een asynchrone functie (met een naam die eindigt op 'Async'). De implementatie van een asynchrone functie initieert het werk op een andere thread en retourneert onmiddellijk met een object dat de asynchrone bewerking vertegenwoordigt. Wanneer de asynchrone bewerking is voltooid, bevat dat geretourneerde object een waarde die het resultaat is van het werk. De naamruimte Windows::Foundation Windows Runtime bevat vier typen asynchrone bewerkingsobjecten.

Elk van deze asynchrone bewerkingstypen wordt geprojecteerd in een bijbehorend type in de winrt::Windows::Foundation C++/WinRT-naamruimte. C++/WinRT bevat ook een interne await adapter struct. U gebruikt deze niet rechtstreeks, maar dankzij die struct kunt u een co_await instructie schrijven om samen te wachten op het resultaat van een functie die een van deze asynchrone bewerkingstypen retourneert. En u kunt uw eigen coroutines schrijven die deze typen retourneren.

Een voorbeeld van een asynchrone Windows functie is SyndicationClient::RetrieveFeedAsync, dat een asynchroon bewerkingsobject retourneert van het type IAsyncOperationWithProgress<TResult, TProgress>.

Laten we eens kijken naar een aantal manieren — eerst blokkerend en daarna niet-blokkerend — om C++/WinRT te gebruiken om een API zoals die aan te roepen. Ter illustratie van de basisideeën gebruiken we een Windows Console Application-project (C++/WinRT) in de volgende paar codevoorbeelden. Technieken die geschikter zijn voor een UI-toepassing, worden besproken in Geavanceerde gelijktijdigheid en asynchroonheid.

De aanroepende thread blokkeren

In het onderstaande codevoorbeeld ontvangt u van RetrieveFeedAsync een object voor een asynchrone bewerking en wordt voor dat object get aangeroepen om de aanroepende thread te blokkeren totdat de resultaten van de asynchrone bewerking beschikbaar zijn.

Als u dit voorbeeld rechtstreeks wilt kopiëren en plakken in het hoofdbroncodebestand van een Windows-consoletoepassingsproject (C++/WinRT), stel dan eerst Geen vooraf gecompileerde headers gebruiken in de projecteigenschappen in.

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void ProcessFeed()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
    // use syndicationFeed.
}

int main()
{
    winrt::init_apartment();
    ProcessFeed();
}

Het aanroepen van get maakt het coderen gemakkelijk en is ideaal voor console-apps of achtergrondthreads wanneer u om wat voor reden dan ook geen coroutine wilt gebruiken. Maar het is niet gelijktijdig of asynchroon, dus het is niet geschikt voor een UI-thread (en een assertie wordt geactiveerd in niet-geoptimaliseerde builds als u deze op één probeert te gebruiken). Om te voorkomen dat we OS-threads tegenhouden om ander nuttig werk te doen, hebben we een andere techniek nodig.

Een coroutine schrijven

C++/WinRT integreert C++ coroutines in het programmeermodel om op een natuurlijke manier coöperatief op een resultaat te wachten. U kunt uw eigen Windows Runtime asynchrone bewerking produceren door een coroutine te schrijven. In het onderstaande codevoorbeeld is ProcessFeedAsync de coroutine.

Note

De get-functie bestaat op het projectietype C++/WinRT winrt::Windows::Foundation::IAsyncAction, zodat u de functie vanuit elk C++/WinRT-project kunt aanroepen. U vindt de functie die wordt vermeld als lid van de IAsyncAction-interface niet, omdat get geen deel uitmaakt van het ABI-oppervlak (Application Binary Interface) van het werkelijke Windows Runtime type IAsyncAction.

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

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

int main()
{
    winrt::init_apartment();

    auto processOp{ ProcessFeedAsync() };
    // do other work while the feed is being printed.
    processOp.get(); // no more work to do; call get() so that we see the printout before the application exits.
}

Een coroutine is een functie die kan worden onderbroken en hervat. In de bovenstaande ProcessFeedAsync-coroutine wordt, wanneer de instructie co_await wordt bereikt, de aanroep naar RetrieveFeedAsync asynchroon gestart; vervolgens schort de coroutine zichzelf onmiddellijk op en geeft de uitvoering terug aan de aanroeper (in het bovenstaande voorbeeld is dat main). main kan vervolgens blijven werken terwijl de feed wordt opgehaald en afgedrukt. Wanneer dat is gebeurd (wanneer de aanroep van RetrieveFeedAsync is voltooid), wordt de ProcessFeedAsync-coroutine hervat bij de volgende instructie.

U kunt een coroutine aggregeren in andere coroutines. Of u kunt get aanroepen om te blokkeren en te wachten tot deze is voltooid (en het resultaat op te halen, als dat er is). U kunt deze ook doorgeven aan een andere programmeertaal die ondersteuning biedt voor de Windows Runtime.

Het is ook mogelijk om de voltooide en/of voortgangsgebeurtenissen van asynchrone acties en bewerkingen af te handelen met behulp van gemachtigden. Zie Gedelegeerde typen voor asynchrone acties en bewerkingen voor meer informatie en codevoorbeelden.

Zoals u kunt zien, blijven we in het bovenstaande codevoorbeeld de blokkerende functieaanroep get gebruiken vlak voordat we main afsluiten. Maar dat is alleen om te voorkomen dat de toepassing wordt beëindigd voordat de uitvoer volledig is afgedrukt.

Asynchroon een Windows Runtime type retourneren

In dit volgende voorbeeld verpakken we een aanroep naar RetrieveFeedAsync, voor een specifieke URI, om ons een RetrieveBlogFeedAsync-functie te geven die asynchroon een SyndicationFeed retourneert.

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

IAsyncOperationWithProgress<SyndicationFeed, RetrievalProgress> RetrieveBlogFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    return syndicationClient.RetrieveFeedAsync(rssFeedUri);
}

int main()
{
    winrt::init_apartment();

    auto feedOp{ RetrieveBlogFeedAsync() };
    // do other work.
    PrintFeed(feedOp.get());
}

In het bovenstaande voorbeeld retourneert RetrieveBlogFeedAsync een IAsyncOperationWithProgress, die zowel voortgang als een retourwaarde heeft. We kunnen ander werk doen terwijl RetrieveBlogFeedAsync zijn ding doet en de feed ophaalt. Vervolgens roepen we get aan op dat object voor asynchrone bewerkingen om te blokkeren, te wachten totdat de bewerking is voltooid en vervolgens de resultaten van de bewerking op te halen.

Als u asynchroon een Windows Runtime type retourneert, moet u een IAsyncOperation<TResult> of een IAsyncOperationWithProgress<TResult, TProgress>, retourneren. Elke eerste of externe runtimeklasse komt in aanmerking, of elk type dat kan worden doorgegeven aan of van een Windows Runtime-functie (bijvoorbeeldint, of winrt::hstring). De compiler helpt u met de fout 'T moet WinRT-type zijn' als u een van deze asynchrone bewerkingstypen probeert te gebruiken met een niet-Windows Runtime type.

Als een coroutine niet ten minste één co_await instructie heeft, moet deze ten minste één co_return of één co_yield instructie hebben om in aanmerking te komen als coroutine. Er zijn gevallen waarin uw coroutine een waarde kan retourneren zonder enige asynchroniteit te introduceren, en dus zonder te blokkeren of van context te wisselen. Hier volgt een voorbeeld dat dat doet (de tweede en volgende keer dat deze wordt aangeroepen) door een waarde in de cache op te cachen.

winrt::hstring m_cache;

IAsyncOperation<winrt::hstring> ReadAsync()
{
    if (m_cache.empty())
    {
        // Asynchronously download and cache the string.
    }
    co_return m_cache;
}

Asynchroon een niet-Windows-Runtime type retourneren

Als u asynchroon een type retourneert dat geen een Windows Runtime-type is, moet u een Parallel Patterns Library (PPL) concurrency::task retourneren. We raden concurrency::task aan omdat deze u betere prestaties biedt (en voor de toekomst een betere compatibiliteit) dan std::future.

Hint

Als u <pplawait.h> opneemt, kunt u concurrency::task gebruiken als coroutinetype.

// main.cpp
#include <iostream>
#include <ppltasks.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

concurrency::task<std::wstring> RetrieveFirstTitleAsync()
{
    return concurrency::create_task([]
        {
            Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
            SyndicationClient syndicationClient;
            SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
            return std::wstring{ syndicationFeed.Items().GetAt(0).Title().Text() };
        });
}

int main()
{
    winrt::init_apartment();

    auto firstTitleOp{ RetrieveFirstTitleAsync() };
    // Do other work here.
    std::wcout << firstTitleOp.get() << std::endl;
}

Parameteroverdracht

Voor synchrone functies moet u standaard parameters gebruiken const& . Dat voorkomt de overhead van kopieën maken (waarbij referenties worden geteld, en dat betekent atomaire verhogingen en verlagingen).

// Synchronous function.
void DoWork(Param const& value);

Maar u kunt problemen ondervinden als u een verwijzingsparameter doorgeeft aan een coroutine.

// NOT the recommended way to pass a value to a coroutine!
IASyncAction DoWorkAsync(Param const& value)
{
    // While it's ok to access value here...

    co_await DoOtherWorkAsync(); // (this is the first suspension point)...

    // ...accessing value here carries no guarantees of safety.
}

In een coroutine is de uitvoering synchroon tot aan het eerste suspensiepunt, waar de controle wordt teruggegeven aan de aanroeper en het aanroepende frame uit scope raakt. Op het moment dat de coroutine wordt hervat, is er mogelijk iets gebeurd met de bronwaarde waarnaar een verwijzingsparameter verwijst. Vanuit het perspectief van de coroutine heeft een verwijzingsparameter onbeheerde levensduur. Dus in het bovenstaande voorbeeld kunnen we value veilig benaderen tot aan de co_await, maar niet erna. Als waarde door de beller wordt vernietigd, leidt een poging om er daarna binnen de coroutine toegang toe te krijgen tot geheugencorruptie. We kunnen value evenmin veilig doorgeven aan DoOtherWorkAsync als het risico bestaat dat die functie op zijn beurt wordt onderbroken en vervolgens na hervatting value probeert te gebruiken.

Als u parameters veilig wilt gebruiken na het onderbreken en hervatten, moeten uw coroutines standaard pass-by-value gebruiken om ervoor te zorgen dat ze op waarde worden vastgelegd en levensduurproblemen voorkomen. Gevallen waarin u afwijkt van die richtlijnen omdat u er zeker van bent dat het veilig is om dit te doen, zal zeldzaam zijn.

// Coroutine
IASyncAction DoWorkAsync(Param value); // not const&

Doorgeven door waarde vereist dat het argument goedkoop is om te verplaatsen of te kopiëren; en dat is meestal het geval voor een slimme aanwijzer.

Er valt ook te beargumenteren dat het, tenzij je de waarde wilt verplaatsen, goed gebruik is om per const-waarde door te geven. Het heeft geen effect op de bronwaarde van waaruit u een kopie maakt, maar het maakt de intentie duidelijk en helpt als u de kopie per ongeluk wijzigt.

// coroutine with strictly unnecessary const (but arguably good practice).
IASyncAction DoWorkAsync(Param const value);

Zie ook Standaardarrays en vectoren, waarin wordt uitgelegd hoe u een standaardvector doorgeeft aan een asynchroon aangeroepen functie.

Als u de handtekening van uw coroutine niet kunt wijzigen, maar u kunt de implementatie wijzigen, kunt u een lokale kopie maken vóór de eerste co_await.

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_value = value;
    // It's ok to access both safe_value and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_value here (not value).
}

Als Param het duur is om te kopiëren, pak dan alleen de stukken die u nodig hebt voor de eerste co_await.

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_data = value.data;
    // It's ok to access safe_data, value.data, and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_data here (not value.data, nor value).
}

Veilige toegang tot de this-pointer in een coroutine van een klasselid

Zie sterke en zwakke verwijzingen in C++/WinRT.

Belangrijke API's