Parallelität und asynchrone Vorgänge mit C++/WinRT

Important

In diesem Thema werden die Konzepte von Coroutines und co_await vorgestellt, deren Verwendung wir sowohl in Ihrer UI als auch in Ihren Nicht-UI-Anwendungen empfehlen. Der Einfachheit halber zeigen die meisten Codebeispiele in diesem Einführungsthema Windows C++/WinRT-Projekte (Console Application). Die späteren Codebeispiele in diesem Abschnitt verwenden Coroutinen, aber der Einfachheit halber verwenden die Konsolenanwendungsbeispiele auch weiterhin den blockierenden Funktionsaufruf get direkt vor dem Beenden, damit die Anwendung nicht beendet wird, bevor ihre Ausgabe vollständig ausgegeben wurde. Das wirst du nicht von einem UI-Thread aus tun (die blockierende Funktion get aufrufen). Stattdessen verwenden Sie die co_await Anweisung. Die Techniken, die Sie in Ihren UI-Anwendungen verwenden, werden im Thema "Erweiterte Parallelität und Asynchronität" beschrieben.

In diesem einführenden Thema werden einige Möglichkeiten gezeigt, wie Sie mit C++/WinRT asynchrone Objekte der Windows-Runtime erstellen und verwenden können. Nach dem Lesen dieses Themas, insbesondere für Techniken, die Sie in Ihren UI-Anwendungen verwenden werden, finden Sie auch weitere Informationen zur erweiterten Parallelität und Asynchronität.

Asynchrone Vorgänge und Windows-Runtime "Async"-Funktionen

Jede Windows-Runtime-API, deren Ausführung möglicherweise länger als 50 Millisekunden dauert, wird als asynchrone Funktion implementiert (deren Name auf "Async" endet). Die Implementierung einer asynchronen Funktion startet die Arbeit in einem anderen Thread und kehrt sofort mit einem Objekt zurück, das den asynchronen Vorgang darstellt. Wenn der asynchrone Vorgang abgeschlossen ist, enthält das zurückgegebene Objekt einen beliebigen Wert, der aus der Arbeit resultierte. Der namespace Windows::Foundation Windows-Runtime enthält vier Typen von asynchronen Vorgangsobjekten.

Jeder dieser asynchronen Vorgangstypen wird in einen entsprechenden Typ im winrt::Windows::Foundation C++/WinRT-Namespace projiziert. C++/WinRT enthält auch eine interne Struktur des Await-Adapters. Sie verwenden sie nicht direkt, aber dank dieser Struktur können Sie eine co_await Anweisung schreiben, um kooperativ auf das Ergebnis jeder Funktion zu warten, die einen dieser asynchronen Vorgangstypen zurückgibt. Und Sie können Ihre eigenen Coroutinen erstellen, die diese Typen zurückgeben.

Ein Beispiel für eine asynchrone Windows-Funktion ist SyndicationClient::RetrieveFeedAsync, das ein asynchrones Vorgangsobjekt vom Typ IAsyncOperationWithProgress<TResult, TProgress> zurückgibt.

Sehen wir uns einige Möglichkeiten an, C++/WinRT zum Aufrufen einer API wie dieser zu verwenden – zunächst auf blockierende und dann auf nicht blockierende Weise. Zur Veranschaulichung der grundlegenden Ideen verwenden wir in den nächsten Codebeispielen ein Windows Konsolenanwendungsprojekt (C++/WinRT). Techniken, die für eine UI-Anwendung besser geeignet sind, werden in erweiterter Parallelität und Asynchronität erläutert.

Blockieren des aufrufenden Threads

Das folgende Codebeispiel erhält von RetrieveFeedAsync ein Objekt einer asynchronen Operation und ruft für dieses Objekt get auf, um den aufrufenden Thread zu blockieren, bis die Ergebnisse der asynchronen Operation verfügbar sind.

Wenn Sie dieses Beispiel direkt in die Hauptquellcodedatei eines Windows Konsolenanwendungsprojekts (C++/WinRT) kopieren möchten, legen Sie zuerst "Not Using Precompiled Headers" in Projekteigenschaften fest.

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

Das Aufrufen von get erleichtert das Programmieren und ist ideal für Konsolenanwendungen oder Hintergrundthreads, bei denen Sie aus welchem Grund auch immer keine Coroutine verwenden möchten. Es ist jedoch weder konkurrierend noch asynchron und daher für einen UI-Thread nicht geeignet (und in nicht optimierten Builds wird eine Assert-Anweisung ausgelöst, wenn Sie versuchen, es in einem solchen Thread zu verwenden). Um zu vermeiden, dass OS-Threads daran gehindert werden, andere nützliche Aufgaben auszuführen, benötigen wir eine andere Technik.

Eine Coroutine schreiben

C++/WinRT integriert C++-Coroutinen in das Programmiermodell, um eine natürliche Möglichkeit zu bieten, kooperativ auf ein Ergebnis zu warten. Sie können Ihren eigenen asynchronen Vorgang für die Windows-Runtime implementieren, indem Sie eine Coroutine schreiben. Im folgenden Codebeispiel ist ProcessFeedAsync die Coroutine.

Note

Die Get-Funktion ist im C++/WinRT-Projektionstyp winrt::Windows::Foundation::IAsyncAction vorhanden, sodass Sie die Funktion innerhalb eines beliebigen C++/WinRT-Projekts aufrufen können. Die Funktion, die als Mitglied der IAsyncAction-Schnittstelle aufgeführt ist, wird nicht gefunden, da get nicht Teil der ABI-Oberfläche (Application Binary Interface) der tatsächlichen Windows-Runtime Typ IAsyncAction ist.

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

Eine Coroutine ist eine Funktion, die suspendiert und wieder aufgenommen werden kann. In der obigen ProcessFeedAsync-Coroutine wird, wenn die co_awaitAnweisung erreicht wird, der Aufruf von RetrieveFeedAsync asynchron initiiert; anschließend setzt sich die Coroutine sofort selbst aus und gibt die Steuerung an den Aufrufer zurück (der im obigen Beispiel main ist). main kann dann weiterarbeiten, während der Feed abgerufen und gedruckt wird. Sobald dies abgeschlossen ist (wenn der Aufruf von RetrieveFeedAsync abgeschlossen ist), wird die ProcessFeedAsync-Koroutine mit der nächsten Anweisung fortgesetzt.

Sie können einen Coroutin in andere Coroutinen aggregieren. Oder Sie können get aufrufen, um zu blockieren und zu warten, bis der Vorgang abgeschlossen ist (und das Ergebnis abzurufen, falls es eines gibt). Sie können sie auch an eine andere Programmiersprache übergeben, die den Windows-Runtime unterstützt.

Es ist auch möglich, die Abschluss- und/oder Fortschrittsereignisse asynchroner Aktionen und Vorgänge mithilfe von Delegaten zu behandeln. Ausführliche Informationen und Codebeispiele finden Sie unter Delegattypen für asynchrone Aktionen und Vorgänge.

Wie Sie sehen können, verwenden wir im obigen Codebeispiel weiterhin den blockierenden Funktionsaufruf get unmittelbar vor dem Verlassen von main. Dies ist jedoch nur so, dass die Anwendung nicht beendet wird, bevor sie den Druckvorgang fertig stellen.

Einen Windows-Runtime-Typ asynchron zurückgeben

In diesem nächsten Beispiel wird ein Aufruf von RetrieveFeedAsync für einen bestimmten URI umbrochen, um uns eine RetrieveBlogFeedAsync-Funktion zu geben, die asynchron einen SyndicationFeed zurückgibt.

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

Im obigen Beispiel gibt RetrieveBlogFeedAsync eine IAsyncOperationWithProgress zurück, die sowohl Den Fortschritt als auch einen Rückgabewert aufweist. Wir können andere Aufgaben ausführen, während RetrieveBlogFeedAsync seine Aufgabe ausführt und den Feed abruft. Anschließend rufen wir get für dieses asynchrone Vorgangsobjekt auf, um die Ausführung zu blockieren, auf den Abschluss des Vorgangs zu warten und anschließend die Ergebnisse des Vorgangs zu erhalten.

Wenn Sie asynchron einen Windows-Runtime-Typ zurückgeben, sollten Sie ein IAsyncOperation<TResult> oder ein IAsyncOperationWithProgress<TResult, TProgress>, zurückgeben. Jede Runtime-Klasse von Erst- oder Drittanbietern kommt infrage, ebenso wie jeder Typ, der an eine Windows-Runtime-Funktion übergeben oder von ihr zurückgegeben werden kann (z. B. int oder winrt::hstring). Der Compiler gibt den Fehler "T must be WinRT type" aus, wenn Sie versuchen, einen dieser asynchronen Vorgangstypen mit einem Nicht-Windows-Runtime-Typ zu verwenden.

Wenn eine Coroutine nicht mindestens eine co_await-Anweisung enthält, muss sie, um als Coroutine zu gelten, mindestens eine co_return- oder eine co_yield-Anweisung enthalten. Es wird Fälle geben, in denen Ihre Coroutine einen Wert zurückgeben kann, ohne Asynchronität einzuführen und damit ohne zu blockieren oder den Kontext zu wechseln. Hier ist ein Beispiel, das dies beim zweiten und bei allen weiteren Aufrufen durch das Zwischenspeichern eines Werts tut.

winrt::hstring m_cache;

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

Asynchrone Rückgabe eines Nicht-Windows-Runtime-Typs

Wenn Sie asynchron einen Typ zurückgeben, der kein Windows-Runtime-Typ ist, sollten Sie eine Parallel Patterns Library (PPL) concurrency::task zurückgeben. Wir empfehlen concurrency::task, da es Ihnen eine höhere Leistung (und künftig eine bessere Kompatibilität) bietet als std::future.

Tipp

Wenn Sie <pplawait.h> einbinden, können Sie concurrency::task als Coroutine-Typ verwenden.

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

Parameterübergabe

Bei synchronen Funktionen sollten Sie standardmäßig Parameter verwenden const& . Dadurch wird der Aufwand von Kopien vermieden (was die Verweiszählung umfasst, und das bedeutet, dass inKrementierung und Dekrementierung miteinander verzahnt werden).

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

Sie können jedoch auf Probleme stoßen, wenn Sie einer Coroutine einen Referenzparameter übergeben.

// 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 einer Coroutine ist die Ausführung bis zum ersten Suspendierungspunkt synchron, an dem die Kontrolle an den Aufrufer zurückgegeben wird und der Aufruf-Frame aus dem Gültigkeitsbereich gerät. Bis die Coroutine fortgesetzt wird, kann mit dem Quellwert, auf den sich ein Referenzparameter bezieht, alles Mögliche geschehen sein. Aus der Perspektive von Coroutine hat ein Verweisparameter eine unkontrollierte Lebensdauer. Im obigen Beispiel können wir also bis zum co_await bedenkenlos auf Wert zugreifen, danach jedoch nicht. Falls value vom Aufrufer zerstört wird, führt ein anschließender Versuch, innerhalb der Koroutine darauf zuzugreifen, zu einer Speicherbeschädigung. Wir können value auch nicht sicher an DoOtherWorkAsync übergeben, wenn das Risiko besteht, dass diese Funktion ihrerseits ausgesetzt wird und nach der Wiederaufnahme versucht, value zu verwenden.

Damit Parameter nach dem Anhalten und Fortsetzen sicher verwendet werden können, sollten Ihre Coroutinen standardmäßig pass-by-value verwenden, um sicherzustellen, dass sie nach Wert erfasst werden und Lebensdauerprobleme vermieden werden. Fälle, in denen Sie von dieser Anleitung abweichen können, da Sie sicher sind, dass dies sicher ist, werden selten sein.

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

Die Übergabe per Wert setzt voraus, dass sich das Argument mit geringem Aufwand verschieben oder kopieren lässt; das ist bei einem Smart Pointer typischerweise der Fall.

Man kann auch argumentieren, dass die Übergabe per const-Wert (es sei denn, Sie möchten den Wert verschieben) eine gute Praxis ist. Es hat keine Auswirkungen auf den Quellwert, aus dem Sie eine Kopie erstellen, aber es macht die Absicht klar und hilft, wenn Sie die Kopie versehentlich ändern.

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

Siehe auch Standardarrays und Vektoren, die sich mit dem Übergeben eines Standardvektors an einen asynchronen Angerufenen befassen.

Wenn Sie die Signatur Ihrer Coroutine nicht ändern können, aber die Implementierung ändern können, dann können Sie vor dem ersten co_await eine lokale Kopie anlegen.

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

Wenn das Kopieren von Param teuer ist, extrahieren Sie vor dem ersten co_await nur die benötigten Teile.

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

Sicherer Zugriff auf den this-Zeiger in einer Klassenmember-Koroutine

Siehe starke und schwache Verweise in C++/WinRT.

Wichtige APIs