Asynchrone Programmierung in C++ (Windows-Runtime-Apps)

Applies to Windows and Windows Phone

In diesem Artikel wird die empfohlene Vorgehensweise zur Verwendung asynchroner Methoden in Visual C++-Komponentenerweiterungen (C++/CX) mithilfe der task-Klasse beschrieben, die im concurrency-Namespace in "ppltasks.h" definiert wird.

Lesen Sie auch Muster und Tipps für die asynchrone Programmierung in Hilo (Windows Store-Apps mit C++ und XAML), um zu erfahren, wie wir mithilfe der Concurrency Runtime asynchrone Vorgänge in Hilo (eine Windows-Runtime-App mit C++ und XAML) implementiert haben.

Asynchrone Typen in der Windows-Runtime

Die Windows-Runtime umfasst ein ausgearbeitetes Modell für den Aufruf asynchroner Methoden und stellt die Typen bereit, die Sie für die Verwendung dieser Methoden benötigen. Wenn Sie mit dem asynchronen Modell der Windows-Runtime nicht vertraut sind, sollten Sie den Artikel Asynchrone Programmierung lesen, bevor Sie mit diesen Erläuterungen fortfahren.

Sie können die asynchronen Windows-Runtime-APIs zwar direkt in C++ verwenden, aber es ist besser, die task class und die zugehörigen Typen und Funktionen zu verwenden. Diese sind im concurrency-Namespace enthalten und in <ppltasks.h> definiert. Die concurrency::task-Klasse ist eine Klasse für allgemeine Zwecke. Bei Verwendung des /ZW-Compilerschalters (der für Windows-Runtime-Apps und die entsprechenden Komponenten benötigt wird) werden jedoch die asynchronen Typen der Windows-Runtime durch die task-Klasse gekapselt, was folgende Aufgaben vereinfacht:

  • Verkettung mehrerer asynchroner und synchroner Vorgänge

  • Behandlung von Ausnahmen in Aufgabenabfolgen

  • Ausführung von Abbrüchen in Aufgabenabfolgen

  • Gewährleistung, dass einzelne Aufgaben im richtigen Threadkontext oder -apartment ausgeführt werden

Dieser Artikel bietet einen allgemeinen Überblick über die Verwendung der task-Klasse mit den asynchronen Windows-Runtime-APIs. Die vollständige Dokumentation zur task-Klasse und den dazugehörigen Methoden (einschließlich create_task) finden Sie unter Task-Parallelität (Concurrency Runtime). Weitere Informationen zum Erstellen öffentlicher asynchroner Methoden für die Verwendung durch JavaScript und andere Windows-Runtime-kompatible Sprachen finden Sie unter Erstellen asynchroner Vorgänge in C++ für Windows-Runtime-Apps.

Verwendung asynchroner Vorgänge mit Aufgaben

Im folgenden Beispiel wird gezeigt, wie Sie mit der task-Klasse eine async-Methode verwenden, die eine IAsyncOperation-Schnittstelle zurückgibt und durch deren Vorgang ein Wert erzeugt wird. Die grundlegenden Schritte:

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

  2. Rufen Sie in der Aufgabe die task::then-Memberfunktion auf, und geben Sie einen Lambda-Ausdruck an, der nach Ausführung des asynchronen Vorgangs aufgerufen wird.



#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 task::then-Funktion erstellt und zurückgegeben wird, wird als Fortsetzung bezeichnet. Das Eingabeargument für den benutzerdefinierten Lambda-Ausdruck ist (in diesem Fall) das Ergebnis des beendeten Aufgabenvorgangs. Derselbe Wert würde auch mit einem Aufruf von IAsyncOperation::GetResults bei direkter Verwendung der IAsyncOperation-Schnittstelle abgerufen werden.

Der Aufruf der task::then-Methode wird sofort beendet, und der Delegat wird erst ausgeführt, wenn die asynchronen Vorgänge erfolgreich beendet wurden. In diesem Beispiel wird die Fortsetzung nicht ausgeführt, wenn der asynchrone Vorgang eine Ausnahme auslöst oder aufgrund einer Abbruchanforderung mit einem Abbruch beendet wird. Weiter unten erfahren Sie, wie Sie Fortsetzungen schreiben, die auch bei einem Abbruch oder Fehler der vorhergehenden Aufgabe ausgeführt werden.

Sie deklarieren die Aufgabenvariable zwar im lokalen Stapel, aber die Lebensdauer wird so verwaltet, dass die Variable erst gelöscht wird, wenn alle zugehörigen Vorgänge abgeschlossen sind und alle Verweise auf die Variable außerhalb des Bereichs liegen – auch dann, wenn der Aufruf der Methode vor Abschluss des Vorgangs beendet wird.

Erstellen von Aufgabenabfolgen

Bei der asynchronen Programmierung werden in vielen Fällen Abfolgen von Vorgängen definiert, sogenannte Aufgabenabfolgen, bei denen eine Fortsetzung immer erst ausgeführt wird, wenn die vorhergehende Aufgabe beendet ist. In manchen Fällen erzeugt die letzte (oder vorhergehende) Aufgabe einen Wert, den die Fortsetzung als Eingabe akzeptiert. Mithilfe der task::then-Methode können Sie intuitive und leicht nachvollziehbare Aufgabenabfolgen erstellen. Die Methode gibt ein Element vom Typ task<T> zurück, wobei T der Rückgabetyp der Lambda-Funktion ist. Eine Aufgabenabfolge kann mehrere Fortsetzungen enthalten: myTask.then(…).then(…).then(…);

Aufgabenabfolgen sind insbesondere dann nützlich, wenn eine Fortsetzung einen neuen asynchronen Vorgang erstellt. Solche Aufgaben werden als asynchrone Aufgaben bezeichnet. Das folgende Beispiel zeigt eine Aufgabenabfolge mit zwei Fortsetzungen. Die einleitende Aufgabe ruft das Handle für eine bestehende Datei ab. Nach Abschluss dieses Vorgangs startet die erste Fortsetzung einen neuen asynchronen Vorgang, mit dem die Datei gelöscht wird. Nach Abschluss dieses Vorgangs wird die zweite Fortsetzung ausgeführt, die eine Bestätigungsmeldung ausgibt.



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

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


Das Beispiel oben illustriert vier zentrale Aspekte:

  • Mit der ersten Fortsetzung wird das IAsyncAction^-Objekt in ein Element vom Typ task<void> konvertiert, und das Element vom Typ task wird zurückgegeben.

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

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

  • Da die zweite Fortsetzung auf einem Wert basiert, wird sie nicht ausgeführt, wenn der Vorgang, der mit dem Aufruf von DeleteAsync gestartet wurde, eine Ausnahme auslöst.

Hinweis  Aufgabenabfolgen sind nur eine der Möglichkeiten, die task-Klasse zum Erstellen asynchroner Vorgänge zu verwenden. Sie können Vorgänge auch mit den Verknüpfungs- und Auswahloperatoren && und || erstellen. Weitere Informationen finden Sie unter Task-Parallelität (Konkurrenz-Runtime).

Rückgabetypen für Lambda-Funktionen und Aufgaben

Bei Aufgabenfortsetzungen ist der Rückgabetyp der Lambda-Funktion von einem task-Objekt umschlossen. Wenn die Lambda-Funktion den double-Typ zurückgibt, hat die Fortsetzungsaufgabe den task<double>-Typ. Das Aufgabenobjekt ist jedoch so konzipiert, dass es keine unnötig verschachtelten Rückgabetypen erzeugt. Wenn eine Lambda-Funktion den IAsyncOperation<SyndicationFeed^>^-Typ zurückgibt, gibt die Fortsetzung den Typ task<SyndicationFeed^> und nicht task<task<SyndicationFeed^>> oder task<IAsyncOperation<SyndicationFeed^>^>^ zurück. Dieser Vorgang wird als asynchrones Entpacken bezeichnet und sorgt dafür, dass der asynchrone Vorgang in der Fortsetzung vor dem Aufruf der nächsten Fortsetzung beendet wird.

Sehen Sie sich an, wie die Aufgabe in dem Beispiel oben den task<void>-Typ zurückgibt, obwohl die zugehörige Lambda-Funktion ein IAsyncInfo-Objekt zurückgegeben hat. Die folgende Tabelle gibt einen Überblick über die Typkonvertierungen zwischen Lambda-Funktionen und den einschließenden Aufgaben:

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

In vielen Fällen ist es ratsam, Benutzern die Möglichkeit zum Abbrechen eines asynchronen Vorgangs zu geben. In manchen Fällen müssen Sie möglicherweise außerdem einen Vorgang programmgesteuert außerhalb der Aufgabenabfolge abbrechen. Jeder *Async-Rückgabetyp verfügt zwar über eine von IAsyncInfo geerbte Cancel-Methode, aber es ist ungünstig, Methoden von außen Zugriff darauf zu gewähren. Es ist besser, für den Abbruch in einer Aufgabenabfolge mithilfe einer cancellation_token_source ein cancellation_token zu erstellen und das Token dann an den Konstruktor der ursprünglichen Aufgabe zu übergeben. Wird eine asynchrone Aufgabe mit einem Abbruchtoken erstellt und cancellation_token_source::cancel aufgerufen, ruft die Aufgabe im IAsync*-Vorgang automatisch Cancel auf und übergibt die Abbruchanforderung an die Kette der Fortsetzungen. Im folgenden Pseudocode wird dieses grundlegende Konzept illustriert.


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


Beim Abbruch einer Aufgabe wird eine task_canceled-Ausnahme in der Aufgabenabfolge weitergegeben. Wertbasierte Fortsetzungen werden ganz einfach nicht ausgeführt. Aufgabenbasierte Fortsetzungen dagegen lösen beim Aufruf von task::get die Ausnahme aus. Stellen Sie bei Fortsetzungen für die Fehlerbehandlung sicher, dass die task_canceled-Ausnahme explizit an die Fortsetzung übergeben wird. (Diese Ausnahme wird nicht von Platform::Exception abgeleitet.)

Abbruchvorgänge sind kooperativ. Wenn eine Fortsetzung neben dem Aufruf einer Windows-Runtime-Methode zeitintensive Vorgänge ausführt, müssen Sie den Status des Abbruchtokens regelmäßig überprüfen und die Ausführung bei einem Abbruch beenden. Nach der Bereinigung aller in der Fortsetzung zugeordneten Ressourcen rufen Sie cancel_current_task auf, um die betreffende Aufgabe abzubrechen und den Abbruch an alle nachfolgenden wertbasierten Fortsetzungen weiterzugeben. Ein weiteres Beispiel: Sie können Aufgabenabfolgen erstellen, die das Ergebnis von FileSavePicker-Vorgängen darstellen. Wählt der Benutzer die Schaltfläche Abbrechen, wird die IAsyncInfo::Cancel-Methode nicht aufgerufen. Stattdessen wird der Vorgang erfolgreich ausgeführt, allerdings mit der Rückgabe nullptr. 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 Aufgabenabfolgen

Wenn eine Fortsetzung auch dann ausgeführt werden soll, wenn die vorhergehende Aufgabe abgebrochen wurde oder eine Ausnahme ausgelöst hat, müssen Sie die Fortsetzung als aufgabenbasierte Fortsetzung anlegen. Geben Sie dafür die Eingabe für die entsprechende Lambda-Funktion als task<TResult> oder task<void> an, wenn die Lambda-Funktion der vorhergehenden Aufgabe den IAsyncAction^-Typ zurückgibt.

Zur Behandlung von Fehlern und Abbrüchen in Aufgabenabfolgen müssen jedoch nicht alle Fortsetzungen aufgabenbasiert sein, und Sie müssen nicht alle Vorgänge einschließen, die in einem try…catch-Block ausgelöst werden können. Stattdessen können Sie am Ende der Abfolge eine aufgabenbasierte Fortsetzung hinzufügen und alle Fehler dort behandeln. Alle Ausnahmen (einschließlich task_canceled-Ausnahmen) werden entlang der Aufgabenabfolge weitergegeben, wobei wertbasierte Fortsetzungen ausgelassen werden. Auf diese Weise können Sie die Ausnahme in der aufgabenbasierten Fortsetzung für die Fehlerbehandlung behandeln. Das Beispiel oben kann daher mit einer aufgabenbasierten Fortsetzung für die Fehlerbehandlung umgeschrieben werden:


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

    });
}

Bei aufgabenbasierten Fortsetzungen rufen wir die task::get-Memberfunktion auf, um die Aufgabenergebnisse abzurufen. Dabei muss weiterhin task::get aufgerufen werden. Dies gilt auch bei ergebnislosen IAsyncAction-Vorgängen, da task::get auch alle Ausnahmen abruft, die an die Aufgabe weitergegeben wurden. Wenn die Eingabeaufgabe eine Ausnahme speichert, wird diese mit dem Aufruf von task::get ausgelöst. Wenn Sie task::get nicht aufrufen, am Ende der Kette keine aufgabenbasierte Fortsetzung verwenden oder den ausgelösten Ausnahmetyp nicht abfangen, wird eine unobserved_task_exception-Ausnahme ausgelöst, wenn alle Verweise auf die Aufgabe gelöscht wurden.

Sie sollten nur Ausnahmen abfangen, die Sie auch behandeln können. Wenn die App einen Fehler ausgibt, den Sie nicht beheben können, ist es besser, die App abstürzen zu lassen, als sie mit einem unbekannten Status weiter auszuführen. Außerdem sollten Sie grundsätzlich nicht versuchen, die unobserved_task_exception-Ausnahme direkt abzufangen. Diese Ausnahme ist in erster Linie für Diagnosezwecke gedacht. Wenn unobserved_task_exception ausgelöst wird, ist das meist ein Hinweis auf einen Fehler im Code. Die Ursache dafür ist entweder eine Ausnahme, die behandelt werden muss, oder eine nicht behebbare Ausnahme, die durch einen anderen Fehler im Code hervorgerufen wird.

Verwalten des Threadkontexts

Die UI von Windows-Runtime-Apps wird in einem Singlethread-Apartment (STA) ausgeführt. Aufgaben, deren Lambda-Ausdruck IAsyncAction oder IAsyncOperation zurückgibt, sind apartmentfähig. Wird die Aufgabe im STA erstellt, werden standardmäßig auch alle zugehörigen Fortsetzungen darin ausgeführt, sofern Sie nichts anderes angeben. Anders gesagt: Die gesamte Aufgabenabfolge erbt die Apartmentfähigkeit von der übergeordneten Aufgabe. Dieses Verhalten vereinfacht die Interaktion mit UI-Steuerelementen, auf die ausschließlich über das STA Zugriff besteht.

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



#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;
    });
}


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

Sie können den Standardthreadkontext für beide Aufgabenarten außer Kraft setzen, indem Sie die Überladung der task::then-Methode verwenden, die einen task_continuation_context-Kontext verwendet. In manchen Fällen ist es zum Beispiel vorteilhaft, die Fortsetzung einer apartmentfähigen Aufgabe für einen Hintergrundthread zu planen. Dabei können Sie task_continuation_context::use_arbitrary übergeben, um die Vorgänge der Aufgabe für den nächsten verfügbaren Thread in einem Thread mit mehreren Apartments (Multithread-Apartment, MTA) zu planen. Dadurch wird die Leistung der Fortsetzung verbessert, da die entsprechenden Vorgänge nicht mit anderen Vorgängen im UI-Thread synchronisiert sein müssen.

Das folgende Beispiel zeigt einen Fall, in dem es günstig ist, die task_continuation_context::use_arbitrary-Option anzugeben, und illustriert außerdem, in welcher Form der Standardfortsetzungskontext für die Synchronisierung gleichzeitig ablaufender Vorgänge in nicht threadsicheren Auflistungen nützlich ist. In diesem Codebeispiel wird eine Schleife für eine Liste mit URLs für RSS-Feeds ausgeführt, und für jede URL wird zum Abrufen der Feeddaten ein asynchroner Vorgang gestartet. Die Reihenfolge, in der die Feeds abgerufen werden, können wir nicht steuern, und das ist auch nicht wichtig. Mit dem Ende jedes RetrieveFeedAsync-Vorgangs akzeptiert die erste Fortsetzung das SyndicationFeed^-Objekt und initialisiert damit ein App-spezifisches FeedData^-Objekt. Da alle diese Vorgänge voneinander unabhängig sind, können wir durch Angabe des task_continuation_context::use_arbitrary-Fortsetzungskontexts den Zeitaufwand verringern. Nach dem Initialisieren jedes FeedData-Objekts müssen wir dieses jedoch einem Vector hinzufügen – und das ist keine threadsichere Auflistung. Aus diesem Grund erstellen wir eine Fortsetzung und geben task_continuation_context::use_current an, damit alle Aufrufe von Append in demselben ASTA (Application Single-Threaded Apartment)-Kontext erfolgen. task_continuation_context::use_default müssen wir nicht direkt angeben, da es sich um den Standardkontext handelt, aber aus Gründen der Klarheit gehen wir hier dennoch dergestalt vor.


#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 neu erstellte Aufgaben in einer Fortsetzung handelt, erben die Apartmentfähigkeit der ursprünglichen Aufgabe nicht.

Behandeln von Statusaktualisierungen

Methoden mit Unterstützung von IAsyncOperationWithProgress oder IAsyncActionWithProgress aktualisieren regelmäßig den Status eines laufenden Vorgangs, solange dieser nicht beendet ist. Der Status wird dabei unabhängig von dem Konzept von Aufgaben und Fortsetzungen gemeldet. Sie müssen nur den Delegaten für die Progress-Eigenschaft des Objekts angeben. Eine typische Verwendung von Delegaten ist die Aktualisierung einer Statusleiste auf der Benutzeroberfläche.

Verwandte Themen

Erstellen asynchroner Vorgänge in C++ für Windows Store-Apps
Visual C++-Programmiersprachenreferenz
Roadmap für Windows-Runtime-Apps mit C++
Asynchrone Programmierung
Task-Parallelität (Konkurrenz-Runtime)
task class

 

 

Anzeigen:
© 2014 Microsoft