Egyidejűség és aszinkron műveletek a C++/WinRT használatával

Fontos

Ez a témakör bemutatja a korutinok és a co_await fogalmát, amelyek használatát mind a felhasználói felületen, mind a nem felhasználói felülethez kapcsolódó alkalmazásokban javasoljuk. Az egyszerűség kedvéért a bevezető témakör legtöbb kódpéldái Windows konzolalkalmazási (C++/WinRT) projekteket mutatnak be. A témakör későbbi kódpéldái ugyan korutinokat használnak, de a kényelmesebb szemléltetés érdekében a konzolalkalmazás-példák továbbra is a blokkoló get függvényhívást használják közvetlenül a kilépés előtt, hogy az alkalmazás ne lépjen ki, mielőtt befejezné a kimenet kiírását. Ezt nem fogja megtenni (hívja meg a blokkoló get függvényt) egy felhasználói felületi szálból. Ehelyett a(z) co_await utasítást fogja használni. A felhasználói felületi alkalmazásokban használt technikákat a Speciális egyidejűség és aszinkronság című témakör ismerteti.

Ez a bevezető témakör bemutatja, hogyan hozhat létre és használhat Windows-futtatókörnyezet aszinkron objektumokat a C++/WinRT használatával. A témakör elolvasása után, különösen a felhasználói felületi alkalmazásokban használt technikák esetében, tekintse meg a speciális egyidejűséget és aszinkronizálást is.

Aszinkron műveletek és Windows-futtatókörnyezet "Async" függvények

Minden olyan Windows-futtatókörnyezet API, amely 50 ezredmásodpercnél hosszabb időt vehet igénybe, aszinkron függvényként van implementálva (az "Async" végződésű névvel). Az aszinkron függvény implementálása elindítja a munkát egy másik szálon, és azonnal visszaad egy objektumot, amely az aszinkron műveletet jelöli. Amikor az aszinkron művelet befejeződik, a visszaadott objektum minden olyan értéket tartalmaz, amely a munkából származik. Az Windows::Foundation Windows-futtatókörnyezet névtér négy aszinkron műveleti objektumtípust tartalmaz.

Az aszinkron művelettípusok mindegyike a winrt::Windows::Foundation C++/WinRT névtér megfelelő típusába lesz kivetítve. A C++/WinRT egy belső await adapterstruktúrát is tartalmaz. Nem használja közvetlenül, de ennek a szerkezetnek köszönhetően megírhat egy utasítást co_await , amely együttműködve várja az aszinkron művelettípusok egyikét visszaadó függvény eredményét. Saját koroutinokat is létrehozhat, amelyek ezeket a típusokat adják vissza.

Az aszinkron Windows függvényre példa a SyndicationClient::RetrieveFeedAsync, amely egy IAsyncOperationWithProgress<TResult, TProgress> típusú aszinkron műveleti objektumot ad vissza.

Nézzünk meg néhány módot arra, hogyan használhatjuk a C++/WinRT-t egy ilyen API meghívására — először blokkoló, majd nem blokkoló módon. Csak az alapötletek szemléltetéséhez egy Windows konzolalkalmazási (C++/WinRT) projektet fogunk használni a következő néhány kódpéldában. A felhasználói felületi alkalmazásokhoz jobban illő technikákat a Speciális egyidejűség és aszinkron beállítás ismerteti.

A hívó szál letiltása

Az alábbi kód példája egy aszinkron műveleti objektumot kap a RetrieveFeedAsynctől, és meghívja az objektumot, hogy blokkolja a hívó szálat, amíg az aszinkron művelet eredményei el nem érhetők.

Ha ezt a példát közvetlenül egy Windows-konzolalkalmazás (C++/WinRT) projekt fő forráskódfájljába szeretné másolni, először állítsa be a Nem előre összeállított fejléceket a projekttulajdonságokban.

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

A get hívása kényelmes kódolást tesz lehetővé, és ideális konzolalkalmazásokhoz vagy háttérszálakhoz, ahol bármilyen okból nem szeretne coroutine-t használni. Ez azonban sem nem párhuzamos, sem nem aszinkron, ezért nem megfelelő UI-szálhoz (és egy aszerció hibát jelez a nem optimalizált buildváltozatokban, ha megpróbálja azon használni). Ahhoz, hogy elkerüljük, hogy az operációs rendszer szálai ne végezhessenek más hasznos munkát, más technikára van szükségünk.

Coroutine írása

A C++/WinRT beépíti a C++ korutinokat a programozási modellbe, így természetes módot kínál arra, hogy együttműködő módon várakozzunk egy eredményre. Saját Windows-futtatókörnyezet aszinkron műveletet hozhat létre egy koroutin megírásával. Az alábbi kód példában a ProcessFeedAsync a koroutin.

Note

A get függvény a winrt::Windows::Foundation::IAsyncAction típusú C++/WinRT-vetítési típuson létezik, így bármely C++/WinRT-projektből meghívhatja a függvényt. Nem találja a függvényt az IAsyncAction interfész tagjaként, mert a get nem része a tényleges Windows-futtatókörnyezet IAsyncAction típusú alkalmazás bináris felületének (ABI) felületének.

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

A korutin olyan függvény, amely felfüggeszthető és újraindítható. A fenti ProcessFeedAsync korutinban, amikor a co_await utasításhoz ér, a korutin aszinkron módon elindítja a RetrieveFeedAsync hívást, majd azonnal felfüggeszti magát, és visszaadja a vezérlést a hívó félnek (ami a fenti példában a main). main ezután tovább végezheti a feladatát, miközben a hírfolyam lekérése és kiírása történik. Amikor ez megtörténik (amikor a RetrieveFeedAsync hívás befejeződik), a ProcessFeedAsync korutin a következő utasítással folytatódik.

A koroutint más koroutinokba összesítheti. Vagy meghívhatja a Get to Block metódust, és megvárhatja, amíg befejeződik (és ha van ilyen, megkapja az eredményt). Vagy átadhatja egy másik programozási nyelvnek, amely támogatja a Windows-futtatókörnyezet.

Az aszinkron műveletek és műveletek befejezett és/vagy előrehaladási eseményeit meghatalmazottak használatával is kezelheti. További részletekért és példakódért tekintse meg az aszinkron műveletek és műveletek delegálási típusait.

Mint látható, a fenti kódpéldában közvetlenül a main függvényből való kilépés előtt továbbra is a blokkoló get függvényhívást használjuk. Ez azonban csak azért van, hogy az alkalmazás ne lépjen ki a kimenet nyomtatásának befejezése előtt.

Aszinkron módon Windows-futtatókörnyezet típust ad vissza

Ebben a következő példában egy adott URI-hoz a RetrieveFeedAsync hívást becsomagoljuk, hogy kapjunk egy RetrieveBlogFeedAsync függvényt, amely aszinkron módon egy SyndicationFeed értéket ad vissza.

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

A fenti példában a RetrieveBlogFeedAsync egy IAsyncOperationWithProgress értéket ad vissza, amely mind az előrehaladást, mind a visszatérési értéket adja vissza. Más műveleteket is végezhetünk, miközben a RetrieveBlogFeedAsync végzi a dolgát és beolvassa a hírcsatornát. Ezután meghívjuk a get metódust azon az aszinkron műveleti objektumon, hogy blokkoljuk a végrehajtást, megvárjuk, amíg befejeződik, majd lekérjük a művelet eredményét.

Ha aszinkron módon ad vissza egy Windows-futtatókörnyezet típust, akkor IAsyncOperation<TResult> vagy IAsyncOperationWithProgress<TResult, TProgress értéket kell visszaadnia>. Bármely első vagy harmadik féltől származó futtatókörnyezeti osztály megfelel, illetve bármely olyan típus is, amely átadható egy Windows-futtatókörnyezet függvénynek, vagy visszaadható onnan (például int, vagy winrt::hstring). A fordító egy "A T-nek WinRT-típusnak kell lennie" hibaüzenettel segít, ha ezek közül az aszinkron művelettípusok közül valamelyiket nem Windows-futtatókörnyezet-típussal próbálja használni.

Ha a koroutin nem rendelkezik legalább egy co_await utasítással, akkor a koroutinnak való minősítéshez legalább egy co_return vagy egy co_yield utasítással kell rendelkeznie. Előfordulhatnak olyan esetek, amikor a koroutin aszinkronizálás nélkül, tehát a környezet blokkolása és váltása nélkül tud értéket visszaadni. Íme egy példa, amely egy érték gyorsítótárazásával teszi ezt meg (a második és az azt követő alkalommal).

winrt::hstring m_cache;

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

Aszinkron módon nemWindows-Runtime típust ad vissza

Ha aszinkron módon olyan típussal tér vissza, amely nem Windows-futtatókörnyezet-típus, akkor a Párhuzamos Minták Könyvtárának (PPL) concurrency::task típusát kell visszaadnia. A concurrency::task használatát javasoljuk, mert jobb teljesítményt (és a jövőben jobb kompatibilitást) biztosít, mint a std::future.

Tipp

Ha beilleszti a(z) <pplawait.h> elemet, akkor a concurrency::task típust korutin típusként használhatja.

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

Paraméterátadás

Szinkron függvények esetén alapértelmezés szerint paramétereket kell használnia const& . Ez elkerüli a másolatok többletterhelését (amely magában foglalja a hivatkozásszámlálást, és ez összekapcsolt növekményeket és hanyatlásokat jelent).

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

De problémákba ütközhet, ha egy referenciaparamétert ad át egy koroutinnak.

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

Egy korutinban a végrehajtás az első felfüggesztési pontig szinkron módon zajlik, amelynél a vezérlés visszakerül a hívóhoz, a hívási keret pedig kikerül a hatókörből. Mire a korutin folytatódik, bármi történhetett azzal a forrásértékkel, amelyre egy referencia-paraméter hivatkozik. A koroutin szempontjából egy referenciaparaméter ellenőrizetlen élettartamú. A fenti példában tehát biztonságosan hozzáférhetünk az értékhez egészen az co_awaitértékig, de utána nem. Abban az esetben, ha a hívó megsemmisíti a value elemet, az ezt követő, korutinon belüli hozzáférési kísérlet memóriasérülést eredményez. A DoOtherWorkAsync függvénynek sem tudjuk biztonságosan átadni a value-t, ha fennáll annak a veszélye, hogy ez a függvény felfüggesztődik, majd a folytatódása után megpróbálja használni a value-t.

Annak érdekében, hogy a paraméterek biztonságosan használhatók legyenek a felfüggesztés és a folytatás után, a koroutinoknak alapértelmezés szerint átmenő értéket kell használniuk, hogy érték szerint rögzítsék őket, és elkerüljék az élettartam-problémákat. Azokban az esetekben, amikor eltérhet az útmutatástól, mert biztos benne, hogy biztonságosan megteheti, ritka lesz.

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

Az érték szerinti átadáshoz az argumentumnak olcsón mozgathatónak vagy másolhatónak kell lennie; és ez az okosmutatók esetében jellemzően így is van.

Az is állítható, hogy (hacsak nem akarja mozgatni az értéket) az érték constként való átadása jó gyakorlat. Nem lesz hatással arra a forrásértékre, amelyből másolatot készít, de egyértelművé teszi a szándékot, és segít, ha véletlenül módosítja a másolatot.

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

Lásd még: Standard tömbök és vektorok, amely azt tárgyalja, hogyan lehet egy standard vektort átadni egy aszinkron módon meghívott függvénynek.

Ha nem tudja módosítani a coroutine aláírását, de módosíthatja a megvalósítást, akkor az első co_awaitelőtt helyi másolatot készíthet.

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

Ha a(z) Param másolása költséges, akkor közvetlenül az első co_await előtt csak a szükséges részeket nyerje ki.

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

A this mutató biztonságos elérése osztálytagi korutinban

Lásd: Erős és gyenge hivatkozások a C++/WinRT alkalmazásban.

Fontos API-k