Souběžnost a asynchronní operace s C++/WinRT

Důležité

Toto téma představuje koncepty korutin a co_await, které doporučujeme používat jak v uživatelském rozhraní tak i v aplikacích bez uživatelského rozhraní. Pro zjednodušení většina příkladů kódu v tomto úvodním tématu ukazuje projekty Windows konzolové aplikace (C++/WinRT). Pozdější příklady kódu v této části sice používají korutiny, ale příklady konzolových aplikací z praktických důvodů i nadále používají blokující volání funkce get těsně před ukončením, aby se aplikace neukončila dříve, než dokončí vypsání výstupu. To nebudete dělat (volat blokující funkci get) ve vlákně uživatelského rozhraní. Místo toho použijete co_await příkaz. Techniky, které budete používat ve svých aplikacích uživatelského rozhraní, jsou popsány v tématu Pokročilé souběžnosti a asynchrony.

Toto úvodní téma ukazuje některé ze způsobů, jak vytvářet a využívat prostředí Windows Runtime asynchronní objekty pomocí C++/WinRT. Po přečtení tohoto tématu, zejména kvůli technikám, které budete používat ve svých UI aplikacích, viz také téma Pokročilá souběžnost a asynchronnost.

Asynchronní operace a asynchronní funkce rozhraní prostředí Windows Runtime „Async“

Jakékoli rozhraní API prostředí Windows Runtime, které může trvat více než 50 milisekund, se implementuje jako asynchronní funkce (s názvem končícím na "Async"). Implementace asynchronní funkce zahájí práci v jiném vlákně a vrátí okamžitě objekt, který představuje asynchronní operaci. Po dokončení asynchronní operace obsahuje vrácený objekt libovolnou hodnotu, která byla výsledkem práce. Obor názvů Windows::Foundation prostředí Windows Runtime obsahuje čtyři typy objektu asynchronní operace.

Každý z těchto typů asynchronních operací je projektován do odpovídajícího typu v oboru názvů winrt::Windows::Foundation C++/WinRT. C++/WinRT obsahuje také interní strukturu adaptéru await. Nepoužíváte ho přímo, ale díky této struktuře můžete napsat co_await příkaz, který bude kooperativně čekat na výsledek jakékoli funkce, která vrátí jeden z těchto typů asynchronních operací. A můžete vytvořit vlastní korutiny, které tyto typy vracejí.

Příkladem asynchronní Windows funkce je SyndicationClient::RetrieveFeedAsync, který vrací asynchronní objekt operace typu IAsyncOperationWithProgress<TResult, TProgress>.

Podívejme se na několik způsobů, jak pomocí C++/WinRT volat takové API — nejprve blokujícím a poté neblokujícím způsobem. Pro ilustraci základních nápadů použijeme projekt konzolové aplikace Windows (C++/WinRT) v několika dalších příkladech kódu. Techniky, které jsou vhodnější pro aplikaci uživatelského rozhraní, jsou popsány v advanced concurrency a asynchrony.

Blokování volajícího vlákna

Následující příklad kódu získá objekt asynchronní operace z RetrieveFeedAsync a u tohoto objektu zavolá metodu get, aby zablokoval volající vlákno, dokud nebudou k dispozici výsledky asynchronní operace.

Pokud chcete tento příklad zkopírovat a vložit přímo do hlavního zdrojového souboru projektu konzolové aplikace Windows (C++/WinRT), pak nejprve nastavte nepoužít předkompilované hlavičky ve vlastnostech projektu.

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

Volání get je pohodlné a ideální pro konzolové aplikace nebo vlákna na pozadí, kde z nějakého důvodu nechcete použít korutinu. Není ale souběžné ani asynchronní, takže není vhodné pro vlákno UI (a v neoptimalizovaných buildech se spustí assert, pokud se jej pokusíte použít v takovém vlákně). Abychom neblokovali vlákna operačního systému, aby nemohla vykonávat jinou užitečnou práci, potřebujeme jiný postup.

Napište korutinu

C++/WinRT integruje korutiny jazyka C++ do programovacího modelu a poskytuje tak přirozený způsob, jak kooperativně čekat na výsledek. Vlastní asynchronní operaci prostředí prostředí Windows Runtime můžete vytvořit napsáním korutiny. V následujícím příkladu kódu je ProcessFeedAsync korutinem.

Note

Funkce get existuje v typu projekce C++/WinRT winrt::Windows::Foundation::IAsyncAction, takže funkci můžete volat z libovolného projektu C++/WinRT. Funkci uvedenou jako člen rozhraní IAsyncAction nenajdete, protože get není součástí plochy binárního rozhraní aplikace (ABI) skutečného prostředí Windows Runtime typu 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.
}

Korutina je funkce, kterou lze pozastavit a znovu spustit. Ve výše uvedené korutině ProcessFeedAsync, když vykonávání dospěje k příkazu co_await, korutina asynchronně zahájí volání RetrieveFeedAsync, poté se okamžitě pozastaví a vrátí řízení zpět volajícímu (což je ve výše uvedeném příkladu main). main pak může pokračovat v práci, zatímco se kanál načítá a vypisuje. Jakmile je to hotové (jakmile se dokončí volání RetrieveFeedAsync), korutina ProcessFeedAsync pokračuje dalším příkazem.

Korutinu lze začlenit do jiných korutin. Nebo můžete zavolat get, zablokovat se a počkat na jeho dokončení (a získat výsledek, pokud nějaký je). Nebo ho můžete předat jinému programovacímu jazyku, který podporuje prostředí Windows Runtime.

Pomocí delegátů je také možné zpracovat dokončené a/nebo průběhové události asynchronních akcí a operací. Podrobnosti a příklady kódu najdete v tématu Typy delegátů pro asynchronní akce a operace.

Jak vidíte, ve výše uvedené ukázce kódu nadále používáme blokující volání funkce get těsně před ukončením funkce main. To je ale jenom proto, aby se aplikace před dokončením tisku výstupu neukončila.

Asynchronně vrátit typ prostředí Windows Runtime

V tomto dalším příkladu zabalíme volání RetrieveFeedAsync pro konkrétní identifikátor URI, abychom získali funkci RetrieveBlogFeedAsync , která asynchronně vrátí SyndicationFeed.

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

V příkladu výše vrací RetrieveBlogFeedAsync operaci IAsyncOperationWithProgress, která obsahuje jak průběh, tak návratovou hodnotu. Můžeme dělat něco jiného, zatímco RetrieveBlogFeedAsync pracuje a načítá informační kanál. Potom na objektu této asynchronní operace zavoláme get, čímž se zablokuje běh, počká se na její dokončení a poté se získají výsledky operace.

Pokud asynchronně vracíte typ prostředí Windows Runtime, měli byste vrátit IAsyncOperation<TResult> nebo IAsyncOperationWithProgress<TResult, TProgress>. Jakákoli třída modulu runtime prvního nebo třetí strany splňuje podmínky nebo jakýkoli typ, který lze předat do nebo z funkce prostředí Windows Runtime (například int, nebo winrt::hstring). Kompilátor vám pomůže s chybou "T musí být typ WinRT", pokud se pokusíte použít jeden z těchto typů asynchronních operací s jiným typem než prostředí Windows Runtime.

Pokud korutina neobsahuje alespoň jeden příkaz co_await, musí, aby byla považována za korutinu, obsahovat alespoň jeden příkaz co_return nebo jeden příkaz co_yield. Existují případy, kdy vaše korutina může vrátit hodnotu, aniž by zavedla jakoukoli asynchronnost, a tedy bez blokování nebo přepnutí kontextu. Zde je příklad, který toho dosáhne (při druhém a každém dalším volání) uložením hodnoty do mezipaměti.

winrt::hstring m_cache;

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

Asynchronně vrátit typ, který není Windows Runtime

Pokud asynchronně vracíte typ, který není typem prostředí Windows Runtime, měli byste vrátit typ concurrency::task z knihovny Parallel Patterns Library (PPL). Doporučujeme concurrency::task, protože poskytuje lepší výkon (a lepší budoucí kompatibilitu) než std::future.

Návod

Pokud zahrnete <pplawait.h>, můžete jako korutinový typ použít concurrency::task .

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

Předávání parametrů

Pro synchronní funkce byste měli ve výchozím nastavení používat const& parametry. Tím se zabrání režijním nákladům na kopie (které zahrnují počítání odkazů, což znamená vzájemné přírůstky a dekrementace).

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

Pokud ale předáte referenční parametr korutině, můžete narazit na problémy.

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

V korutině probíhá vykonávání synchronně až do prvního bodu přerušení, kdy je řízení vráceno volajícímu a volací rámec přestane být v platnosti. Než se korutina znovu spustí, se zdrojovou hodnotou, na kterou odkazuje referenční parametr, se mohlo stát cokoli. Z pohledu korutiny má referenční parametr nezařízenou životnost. V předchozím příkladu tedy máme bezpečný přístup k hodnotě až do co_awaitté doby, ale ne po ní. V případě, že volající zničí hodnotu, vede následný pokus o přístup k ní uvnitř korutiny k poškození paměti. Ani nemůžeme bezpečně předat hodnotu funkci DoOtherWorkAsync, pokud existuje jakékoli riziko, že se tato funkce následně pozastaví a po obnovení se pokusí použít hodnotu.

Aby byly parametry bezpečné pro použití po pozastavení a obnovení, měly by vaše koruty ve výchozím nastavení používat průchozí hodnotu, aby se zajistilo, že zachytí podle hodnoty, a vyhnout se problémům s životností. Případy, kdy se můžete od tohoto doporučení odchýlit, protože jste si jisti, že je to bezpečné, budou ojedinělé.

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

Předání hodnotou vyžaduje, aby přesun nebo zkopírování argumentu bylo nenákladné; což je u inteligentního ukazatele obvykle splněno.

Lze také tvrdit, že (pokud nechcete hodnotu přesunout) předávání konstantní hodnotou je dobrá praxe. Nebude to mít žádný vliv na zdrojovou hodnotu, ze které kopii vytváříte, ale záměr to objasní a pomůže, pokud kopii neúmyslně upravíte.

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

Viz také Standardní pole a vektory, kde se popisuje, jak předat standardní vektor asynchronně volané funkci.

Pokud nemůžete změnit podpis korutiny, ale můžete změnit implementaci, můžete vytvořit místní kopii před prvním 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).
}

Pokud Param je kopírování nákladné, extrahujte pouze ty kusy, které potřebujete před prvním 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).
}

Bezpečný přístup k tomuto ukazateli v korutině člena třídy

Viz silné a slabé odkazy v jazyce C++/WinRT.

Důležitá rozhraní API