Asynchrone Programmierung in C++/CX

Hinweis

Dieses Thema soll Ihnen dabei helfen, Ihre C++/CX-Anwendung zu pflegen. Wir empfehlen aber, C++/WinRT für neue Anwendungen zu nutzen. C++/WinRT ist eine vollständig standardisierte, moderne C++17-Programmiersprache für Windows-Runtime-APIs (WinRT), die als headerdateibasierte Bibliothek implementiert ist und Ihnen einen erstklassigen Zugriff auf die moderne Windows-API bietet.

In diesem Artikel wird die empfohlenene Vorgehensweise zur Verwendung asynchroner Methoden in Visual C++ Komponentenerweiterungen (C++/CX) beschrieben, indem Sie die task-Klasse verwenden, die im concurrency-Namespace in „ppltasks.h“ definiert wird.

Windows Runtime asynchrone Typen

Die Windows-Runtime verfügt über ein gut definiertes Modell zum Aufrufen asynchroner Methoden und stellt die Typen bereit, die Sie für die Nutzung dieser Methoden benötigen. Wenn Sie mit dem asynchronen Windows-Runtime-Modell nicht vertraut sind, lesen Sie Asynchrone Programmierung, bevor Sie den Rest dieses Artikels lesen.

Obwohl Sie die asynchronen Windows-Runtime-APIs direkt in C++ nutzen können, besteht der bevorzugte Ansatz darin, die task-Klasse und die zugehörigen Typen und Funktionen zu verwenden, die im Concurrency-Namespace enthalten und in <ppltasks.h> definiert sind. Der concurrency::task ist ein universeller Typ, aber wenn der /ZW-Compilerschalter, der für Universelle Windows-Plattform-Apps und -Komponenten (UWP) erforderlich ist, verwendet wird, kapselt die Aufgabenklasse die asynchronen Windows-Runtime-Typen, um Folgendes einfacher zu machen:

  • Verketten mehrerer asynchroner und synchroner Vorgänge

  • Behandeln von Ausnahmen in Aufgabenketten

  • Abbruch in Aufgabenketten ausführen

  • Sicherstellen, dass einzelne Aufgaben im entsprechenden Threadkontext oder Apartment ausgeführt werden

Dieser Artikel enthält grundlegende Anleitungen zur Verwendung der task-Klassen mit den asynchronen Windows-Runtime-APIs. Eine ausführlichere Dokumentation zu task und den zugehörigen Methoden, einschließlich create_task, finden Sie unter Aufgabenparallelität (Concurrency Runtime).

Verwenden eines asynchronen Vorgangs mithilfe einer Aufgabe

Das folgende Beispiel zeigt, wie Sie die Augabenklasse verwenden, um eine asynchrone Methode zu verwenden, die eine IAsyncOperation-Schnittstelle zurückgibt und deren Vorgang einen Wert erzeugt. Im Folgenden finden Sie die grundlegenden Schritte:

  1. Rufen Sie die create_task-Methode auf, und übergeben Sie sie an das IAsyncOperation^-Objekt.

  2. Rufen Sie den Memberfunktions-task::then für die Aufgabe auf, und geben Sie eine Lambda-Funktion an, die aufgerufen wird, wenn der asynchrone Vorgang abgeschlossen ist.

#include <ppltasks.h>
using namespace concurrency;
using namespace Windows::Devices::Enumeration;
...
void App::TestAsync()
{    
    //Call the *Async method that starts the operation.
    IAsyncOperation<DeviceInformationCollection^>^ deviceOp =
        DeviceInformation::FindAllAsync();

    // Explicit construction. (Not recommended)
    // Pass the IAsyncOperation to a task constructor.
    // task<DeviceInformationCollection^> deviceEnumTask(deviceOp);

    // Recommended:
    auto deviceEnumTask = create_task(deviceOp);

    // Call the task's .then member function, and provide
    // the lambda to be invoked when the async operation completes.
    deviceEnumTask.then( [this] (DeviceInformationCollection^ devices )
    {       
        for(int i = 0; i < devices->Size; i++)
        {
            DeviceInformation^ di = devices->GetAt(i);
            // Do something with di...          
        }       
    }); // end lambda
    // Continue doing work or return...
}

Die Aufgabe, die von der Funktion task::then erstellt und zurückgegeben wird, wird als Fortsetzung bezeichnet. Das Eingabeargument (in diesem Fall) für die vom Benutzer bereitgestellte Lambda-Funktion ist das Ergebnis, das der Vorgang nach Abschluss des Vorgangs erzeugt. Es ist derselbe Wert, der durch Aufrufen von IAsyncOperation::GetResults abgerufen wird, wenn Sie die IAsyncOperation-Schnittstelle direkt verwenden.

Die Methode task::then gibt sofort zurück, und ihr Delegat wird erst ausgeführt, wenn die asynchrone Arbeit erfolgreich abgeschlossen wurde. Wenn der asynchrone Vorgang in diesem Beispiel bewirkt, dass eine Ausnahme ausgelöst wird oder aufgrund einer Abbruchanforderung im abgebrochenen Zustand endet, wird die Fortsetzung nie ausgeführt. Wir werden später erläutern, wie Fortsetzungen geschrieben werden, die auch dann ausgeführt werden, wenn die vorherige Aufgabe abgebrochen oder fehlgeschlagen ist.

Obwohl Sie die Aufgabenvariable im lokalen Stapel deklarieren, verwaltet sie ihre Lebensdauer so, dass sie erst gelöscht wird, wenn alle zugehörigen Vorgänge abgeschlossen sind und alle Verweise darauf außerhalb des Gültigkeitsbereichs liegen, auch wenn die Methode vor Abschluss der Vorgänge zurückgegeben wird.

Erstellen einer Aufgabenkette

Bei der asynchronen Programmierung ist es üblich, eine Abfolge von Vorgängen zu definieren, die auch als Taskchain bezeichnet wird, in der jede Fortsetzung nur ausgeführt wird, wenn die vorherige ausgeführt wird. In einigen Fällen erzeugt der vorherige Vorgang (bzw. die Vorgänger-Aufgabe) einen Wert, den die Fortsetzung als Eingabe akzeptiert. Mithilfe der task::then-Methode können Sie Aufgabenketten intuitiv und unkompliziert erstellen. Die Methode gibt einen task<T> zurück, wobei T der Rückgabetyp der Lambda-Funktion ist. Sie können mehrere Fortsetzungen in einer Aufgabenkette verfassen: myTask.then(…).then(…).then(…);

Aufgabenketten sind besonders nützlich, wenn eine Fortsetzung einen neuen asynchronen Vorgang erstellt; eine solche Aufgabe wird als asynchrone Aufgabe bezeichnet. Das folgende Beispiel veranschaulicht eine Aufgabenkette mit zwei Fortsetzungen. Die erste Aufgabe ruft das Handle für eine vorhandene Datei ab, und wenn dieser Vorgang abgeschlossen ist, startet die erste Fortsetzung einen neuen asynchronen Vorgang, um die Datei zu löschen. Wenn dieser Vorgang abgeschlossen ist, wird die zweite Fortsetzung ausgeführt und gibt eine Bestätigungsmeldung aus.

#include <ppltasks.h>
using namespace concurrency;
...
void App::DeleteWithTasks(String^ fileName)
{    
    using namespace Windows::Storage;
    StorageFolder^ localFolder = ApplicationData::Current->LocalFolder;
    auto getFileTask = create_task(localFolder->GetFileAsync(fileName));

    getFileTask.then([](StorageFile^ storageFileSample) ->IAsyncAction^ {       
        return storageFileSample->DeleteAsync();
    }).then([](void) {
        OutputDebugString(L"File deleted.");
    });
}

Das vorherige Beispiel verdeutlicht diese vier wichtigen Punkte:

  • Die erste Fortsetzung konvertiert das IAsyncAction^-Objekt in eine task<void> und gibt den task zurück.

  • Die zweite Fortsetzung führt keine Fehlerbehandlung durch und übernimmt daher void und nicht task<void> als Eingabe. Es handelt sich um eine wertbasierte Fortsetzung.

  • Die zweite Fortsetzung wird erst ausgeführt, wenn der DeleteAsync-Vorgang abgeschlossen ist.

  • Da die zweite Fortsetzung wertbasiert ist, wird die zweite Fortsetzung überhaupt nicht ausgeführt, wenn der Vorgang, der vom Aufruf von DeleteAsync gestartet wurde, eine Ausnahme auslöst.

Hinweis Das Erstellen einer Aufgabenkette ist nur eine der Möglichkeiten, die task-Klasse zum Verfassen asynchroner Vorgänge zu verwenden. Sie können Vorgänge auch erstellen, indem Sie die join- und choice-Operatoren && und || verwenden. Weitere Informationen finden Sie unter Aufgabenparallelität (Concurrency Runtime).

Rückgabetypen der Lambda-Funktion und Aufgabenrückgabetypen

In einer Aufgabenfortsetzung wird der Rückgabetyp der Lambda-Funktion in ein task-Objekt verpackt. Wenn die Lambda-Funktion einen Double-Wert zurückgibt, ist der Typ der Fortsetzungsaufgabe Task<double>. Das Taskobjekt ist jedoch so konzipiert, dass es nicht unnötig geschachtelte Rückgabetypen erzeugt. Wenn eine Lambda-Funktion einen IAsyncOperation<SyndicationFeed^>^ zurückgibt, gibt die Fortsetzung einen task<SyndicationFeed^>, nicht eine task<task<SyndicationFeed^>> oder den task<IAsyncOperation<SyndicationFeed^>^>^ zurück. Dieser Prozess wird als asynchrones Entpacken bezeichnet und stellt außerdem sicher, dass der asynchrone Vorgang innerhalb der Fortsetzung abgeschlossen ist, bevor die nächste Fortsetzung aufgerufen wird.

Beachten Sie im vorherigen Beispiel, dass die Aufgabe eine task<void> zurückgibt, obwohl die Lambda-Funktion ein IAsyncInfo-Objekt zurückgegeben hat. In der folgenden Tabelle sind die Typkonvertierungen zusammengefasst, die zwischen einer Lambda-Funktion und der eingeschlossenen Aufgabe auftreten:

Lambda-Rückgabetyp .then Rückgabetyp
TResult task<TResult>
IAsyncOperation<TResult>^ task<TResult>
IAsyncOperationWithProgress<TResult, TProgress>^ task<TResult>
IAsyncAction^ task<void>
IAsyncActionWithProgress<TProgress>^ task<void>
task<TResult> task<TResult>

Abbrechen von Aufgaben

Häufig empfiehlt es sich, dem Benutzer die Möglichkeit zu geben, einen asynchronen Vorgang abzubrechen. In einigen Fällen müssen Sie einen Vorgang programmgesteuert von außerhalb der Aufgabenkette abbrechen. Obwohl jeder *Async-Rückgabetyp über eine Cancel-Methode verfügt, die er von IAsyncInfo erbt, ist es ungünstig, ihn für externe Methoden verfügbar zu machen. Die bevorzugte Möglichkeit zum Unterstützen des Abbruchs in einer Aufgabenkette besteht darin, eine cancellation_token_source zum Erstellen einer cancellation_token zu verwenden und dann das Token an den Konstruktor der ersten Aufgabe zu übergeben. Wenn eine asynchrone Aufgabe mit einem Abbruchtoken erstellt wird und [cancellation_token_source::cancel](/cpp/parallel/concrt/reference/cancellation-token-source-class?view=vs-2017& -view=true) aufgerufen wird, ruft die Aufgabe automatisch Abbrechen für den IAsync*-Vorgang auf und übergibt die Abbruchanforderung an die Fortsetzungskette. Der folgende Pseudocode veranschaulicht den grundlegenden Ansatz.

//Class member:
cancellation_token_source m_fileTaskTokenSource;

// Cancel button event handler:
m_fileTaskTokenSource.cancel();

// task chain
auto getFileTask2 = create_task(documentsFolder->GetFileAsync(fileName),
                                m_fileTaskTokenSource.get_token());
//getFileTask2.then ...

Wenn eine Aufgabe abgebrochen wird, wird eine task_canceled-Ausnahme in der Aufgabenkette weitergegeben. Wertbasierte Fortsetzungen werden einfach nicht ausgeführt, aber aufgabenbasierte Fortsetzungen führen dazu, dass die Ausnahme ausgelöst wird, wenn task::get aufgerufen wird. Wenn Sie eine Fehlerbehandlungs-Fortsetzung haben, stellen Sie sicher, dass die Ausnahme task_canceled explizit abgefangen wird. (Diese Ausnahme ist nicht von Platform::Exception abgeleitet.)

Das Abbrechen ist kooperativ. Wenn Ihre Fortsetzung zeitintensive Arbeit ausführt und es dabei nicht nur um das Aufrufen einer UWP-Methode geht, liegt es in Ihrer Verantwortung, den Status des Abbruchtokens regelmäßig zu überprüfen und die Ausführung zu beenden, wenn sie abgebrochen wird. Nachdem Sie alle Ressourcen, die in der Fortsetzung zugeordnet wurden, aufgeräumt haben, rufen Sie cancel_current_task auf, um diese Aufgabe abzubrechen und den Abbruch an alle wertbasierten Fortsetzungen weiterzugeben, die darauf folgen. Hier ein weiteres Beispiel: Sie können eine Aufgabenkette erstellen, die das Ergebnis eines FileSavePicker-Vorgang darstellt. Wenn der Benutzer die Schaltfläche Abbrechen auswählt, wird die Methode IAsyncInfo::Cancel nicht aufgerufen. Stattdessen ist der Vorgang erfolgreich, gibt aber nullptr zurück. Die Fortsetzung kann den Eingabeparameter testen und cancel_current_task aufrufen, wenn die Eingabe nullptr ist.

Weitere Informationen finden Sie unter Abbruch in der PPL.

Behandeln von Fehlern in einer Aufgabenkette

Wenn eine Fortsetzung auch dann ausgeführt werden soll, wenn der Vorgänger abgebrochen wurde oder eine Ausnahme ausgelöst hat, müssen Sie die Fortsetzung als aufgabenbasierte Fortsetzung festlegen, indem Sie die Eingabe für die Lambda-Funktion als task<TResult> oder task<void> spezifizieren, wenn die Lambda-Funktion der Vorgänger-Aufgabe einen IAsyncAction^-Wert zurückgibt.

Um Fehler und den Abbruch in einer Aufgabenkette zu behandeln, müssen Sie nicht jede Fortsetzung aufgabenbasiert machen oder jeden Vorgang einschließen, der innerhalb eines try…catch-Blocks ausgelöst werden kann. Stattdessen können Sie eine aufgabenbasierte Fortsetzung am Ende der Kette hinzufügen und alle Fehler dort behandeln. Jede Ausnahme - dies umfasst auch eine task_canceled-Ausnahme - wird entlang der Aufgabenkette weitergegeben und umgeht alle wertbasierten Fortsetzungen, sodass Sie sie in der aufgabenbasierten Fortsetzung für die Fehlerbehandlung behandeln können. Wir können das vorherige Beispiel neu schreiben, um eine aufgabenbasierten Fortsetzung für die Fehlerbehandlung zu verwenden:

#include <ppltasks.h>
void App::DeleteWithTasksHandleErrors(String^ fileName)
{    
    using namespace Windows::Storage;
    using namespace concurrency;

    StorageFolder^ documentsFolder = KnownFolders::DocumentsLibrary;
    auto getFileTask = create_task(documentsFolder->GetFileAsync(fileName));

    getFileTask.then([](StorageFile^ storageFileSample)
    {       
        return storageFileSample->DeleteAsync();
    })

    .then([](task<void> t)
    {

        try
        {
            t.get();
            // .get() didn' t throw, so we succeeded.
            OutputDebugString(L"File deleted.");
        }
        catch (Platform::COMException^ e)
        {
            //Example output: The system cannot find the specified file.
            OutputDebugString(e->Message->Data());
        }

    });
}

In einer aufgabenbasierten Fortsetzung rufen wir den Memberfunktions-task::get auf, um die Ergebnisse der Aufgabe abzurufen. Wir müssen task::get auch dann aufrufen, wenn es sich bei dem Vorgang um eine IAsyncAction handelt, die kein Ergebnis erzeugt, da task::get auch alle Ausnahmen abruft, die nach unten zur Aufgabe weitergegeben wurden. Wenn die Eingabeaufgabe eine Ausnahme speichert, wird sie beim Aufruf von task::get ausgelöst. Wenn Sie task::get nicht aufrufen oder keine aufgabenbasierte Fortsetzung am Ende der Kette verwenden oder den ausgelösten Ausnahmetyp nicht abfangen, wird eine unobserved_task_exception ausgelöst, wenn alle Verweise auf die Aufgabe gelöscht wurden.

Fangen Sie nur die Ausnahmen ab, die Sie bewältigen können. Wenn bei Ihrer App ein Fehler auftritt, bei dem keine Wiederherstellung möglich ist, empfiehlt es sich, den Absturz der App zuzulassen, anstatt die Ausführung in einem unbekannten Zustand fortzusetzen. Sie sollten im Allgemeinen nicht versuchen, die unobserved_task_exception selbst aufzufangen. Diese Ausnahme ist überwiegend für Diagnosezwecke vorgesehen. Wenn unobserved_task_exception ausgelöst wird, weist dies in der Regel auf einen Fehler im Code hin. Häufig ist die Ursache entweder eine Ausnahme, die behandelt werden sollte, oder eine nicht behebbare Ausnahme, die durch einen anderen Fehler im Code verursacht wird.

Verwalten des Threadkontexts

Die Benutzeroberfläche einer UWP-App wird in einem Singlethread-Apartment (STA) ausgeführt. Eine Aufgabe, deren Lambda entweder eine IAsyncAction oder IAsyncOperation zurückgibt, ist apartmentfähig. Wenn die Aufgabe im STA erstellt wird, werden alle zugehörigen Fortsetzungen standardmäßig auch darin ausgeführt, es sei denn, Sie geben etwas anderes an. Mit anderen Worten erbt die gesamte Aufgabenkette die Apartmentfähigkeit von der übergeordneten Aufgabe. Dieses Verhalten vereinfacht die Interaktionen mit UI-Steuerelementen, auf die nur über das STA zugegriffen werden kann.

Beispielsweise können Sie in einer UWP-App in der Memberfunktion jeder Klasse, die eine XAML-Seite darstellt, ein ListBox-Steuerelement innerhalb einer task::then-Methode auffüllen, ohne das Dispatcher-Objekt verwenden zu müssen.

#include <ppltasks.h>
void App::SetFeedText()
{    
    using namespace Windows::Web::Syndication;
    using namespace concurrency;
    String^ url = "http://windowsteamblog.com/windows_phone/b/wmdev/atom.aspx";
    SyndicationClient^ client = ref new SyndicationClient();
    auto feedOp = client->RetrieveFeedAsync(ref new Uri(url));

    create_task(feedOp).then([this]  (SyndicationFeed^ feed)
    {
        m_TextBlock1->Text = feed->Title->Text;
    });
}

Wenn eine Aufgabe keine IAsyncAction oder IAsyncOperation zurückgibt, ist sie nicht apartmentfähig und standardmäßig werden die Fortsetzungen im ersten verfügbaren Hintergrundthread ausgeführt.

Sie können den Standardthreadkontext für jede Art von Aufgabe überschreiben, indem Sie die Überladung von task::then verwenden, die einen task_continuation_context verwendet. In einigen Fällen kann es z. B. wünschenswert sein, die Fortsetzung einer apartmentfähigen Aufgabe in einem Hintergrundthread zu planen. In einem solchen Fall können Sie task_continuation_context::use_arbitrary übergeben, um die Arbeit der Aufgabe im nächsten verfügbaren Thread in einem Multithread-Apartment zu planen. Dies kann die Leistung der Fortsetzung verbessern, da ihre Arbeit nicht mit anderen Arbeiten synchronisiert werden muss, die im UI-Thread ausgeführt werden.

Im folgenden Beispiel wird veranschaulicht, wann es nützlich ist, die Option task_continuation_context::use_arbitrary anzugeben, und es zeigt auch, wie der Standardfortsetzungskontext für die Synchronisierung gleichzeitiger Vorgänge in nicht threadsicheren Auflistungen nützlich ist. In diesem Code durchlaufen wir eine Liste der URLs für RSS-Feeds, und für jede URL starten wir einen asynchronen Vorgang zum Abrufen der Feeddaten. Wir können die Reihenfolge, in der die Feeds abgerufen werden, nicht steuern, und sie interessiert uns auch nicht wirklich. Wenn jeder RetrieveFeedAsync-Vorgang abgeschlossen ist, akzeptiert die erste Fortsetzung das SyndicationFeed^-Objekt und verwendet es zum Initialisieren eines app-definierten FeedData^-Objekts. Da jeder dieser Vorgänge von den anderen unabhängig ist, können wir das Ganze möglicherweise beschleunigen, indem wir den Fortsetzungskontext task_continuation_context::use_arbitrary angeben. Nachdem jedoch jedes FeedData-Objekt initialisiert wurde, müssen wir es einem Vector hinzufügen, der keine threadsichere Auflistung ist. Daher erstellen wir eine Fortsetzung und geben [task_continuation_context::use_current](/cpp/parallel/concrt/reference/task-continuation-context-class?view=vs-2017& -view=true) an, um sicherzustellen, dass alle Aufrufe von Append im gleichen Anwendungs-Single-Threaded-Apartment-Kontext (ASTA) ausgeführt werden. Da task_continuation_context::use_default der Standardkontext ist, müssen wir sie nicht explizit angeben, aber wir tun dies hier aus Gründen der Klarheit.

#include <ppltasks.h>
void App::InitDataSource(Vector<Object^>^ feedList, vector<wstring> urls)
{
                using namespace concurrency;
    SyndicationClient^ client = ref new SyndicationClient();

    std::for_each(std::begin(urls), std::end(urls), [=,this] (std::wstring url)
    {
        // Create the async operation. feedOp is an
        // IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^
        // but we don't handle progress in this example.

        auto feedUri = ref new Uri(ref new String(url.c_str()));
        auto feedOp = client->RetrieveFeedAsync(feedUri);

        // Create the task object and pass it the async operation.
        // SyndicationFeed^ is the type of the return value
        // that the feedOp operation will eventually produce.

        // Then, initialize a FeedData object by using the feed info. Each
        // operation is independent and does not have to happen on the
        // UI thread. Therefore, we specify use_arbitrary.
        create_task(feedOp).then([this]  (SyndicationFeed^ feed) -> FeedData^
        {
            return GetFeedData(feed);
        }, task_continuation_context::use_arbitrary())

        // Append the initialized FeedData object to the list
        // that is the data source for the items collection.
        // This all has to happen on the same thread.
        // By using the use_default context, we can append
        // safely to the Vector without taking an explicit lock.
        .then([feedList] (FeedData^ fd)
        {
            feedList->Append(fd);
            OutputDebugString(fd->Title->Data());
        }, task_continuation_context::use_default())

        // The last continuation serves as an error handler. The
        // call to get() will surface any exceptions that were raised
        // at any point in the task chain.
        .then( [this] (task<void> t)
        {
            try
            {
                t.get();
            }
            catch(Platform::InvalidArgumentException^ e)
            {
                //TODO handle error.
                OutputDebugString(e->Message->Data());
            }
        }); //end task chain

    }); //end std::for_each
}

Geschachtelte Aufgaben, bei denen es sich um neue Aufgaben handelt, die in einer Fortsetzung erstellt werden, erben keine Apartmentfähigkeit für die erste Aufgabe.

Übergeben von Statusaktualisierungen

Methoden, die IAsyncOperationWithProgress oder IAsyncActionWithProgress unterstützen, stellen in regelmäßigen Abständen Statusaktualisierungen bereit, während der Vorgang ausgeführt wird, bevor er abgeschlossen wird. Die Statusberichte sind unabhängig von Aufgaben und Fortsetzungen. Sie geben einfach den Delegaten für die Progress-Eigenschaft des Objekts an. Eine typische Verwendung des Delegaten besteht darin, eine Statusanzeige auf der Benutzeroberfläche zu aktualisieren.