Erweiterte Parallelität und Asynchronie mit C++/WinRT

In diesem Thema werden erweiterte Szenarien mit Parallelität und Asynchronie in C++/WinRT beschrieben.

Für eine Einführung in dieses Thema lesen Sie zunächst Nebenläufigkeit und asynchrone Operationen.

Auslagern von Aufgaben an den Windows-Threadpool

Eine Coroutine ist eine Funktion wie jede andere, in der ein Aufrufer blockiert wird, bis eine Funktion die Ausführung an sie zurückgibt. Und die erste Gelegenheit, an dem eine Coroutine zurückkehren kann, ist das erste co_await, co_return oder co_yield.

Bevor Sie also rechenintensive Arbeit in einer Coroutine ausführen, müssen Sie die Ausführung an den Aufrufer zurückgeben (mit anderen Worten einen Unterbrechungspunkt einführen), damit der Aufrufer nicht blockiert wird. Wenn Sie dies nicht bereits tun, indem Sie mit co_await einen anderen Vorgang ausführen, können Sie mit co_await die winrt::resume_background-Funktion verwenden. Das gibt die Kontrolle an den Aufrufer zurück und setzt die Ausführung dann sofort auf einem Threadpool-Thread fort.

Der in der Implementierung verwendete Threadpool ist der low-level Windows Threadpool, sodass er optimal effizient ist.

IAsyncOperation<uint32_t> DoWorkOnThreadPoolAsync()
{
    co_await winrt::resume_background(); // Return control; resume on thread pool.

    uint32_t result;
    for (uint32_t y = 0; y < height; ++y)
    for (uint32_t x = 0; x < width; ++x)
    {
        // Do compute-bound work here.
    }
    co_return result;
}

Programmieren unter Berücksichtigung der Threadaffinität

Dieses Szenario baut auf dem vorherigen Szenario auf. Sie entladen einige Arbeit in den Threadpool, aber dann möchten Sie den Fortschritt auf der Benutzeroberfläche (UI) anzeigen.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    textblock.Text(L"Done!"); // Error: TextBlock has thread affinity.
}

Der obige Code löst eine winrt::hresult_wrong_thread Ausnahme aus, da ein TextBlock aus dem Thread aktualisiert werden muss, der ihn erstellt hat, was der UI-Thread ist. Eine Lösung besteht darin, den Threadkontext zu erfassen, in dem unsere Coroutine ursprünglich aufgerufen wurde. Erstellen Sie dazu eine Instanz des Objekts winrt::apartment_context, führen Sie Hintergrundaufgaben durch, und verwenden Sie dann co_await den apartment_context, um wieder zum aufrufenden Kontext zu wechseln.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    winrt::apartment_context ui_thread; // Capture calling context.

    co_await winrt::resume_background();
    // Do compute-bound work here.

    co_await ui_thread; // Switch back to calling context.

    textblock.Text(L"Done!"); // Ok if we really were called from the UI thread.
}

Solange die oben genannte Coroutine aus dem UI-Thread aufgerufen wird, der den TextBlock erstellt hat, funktioniert diese Technik. In Ihrer App wird es viele Fälle geben, in denen Sie sich dessen sicher sind.

Für eine allgemeinere Lösung zum Aktualisieren der Benutzeroberfläche, die Fälle behandelt, in denen Sie über den aufrufenden Thread unsicher sind, können co_await Sie die Winrt::resume_foreground-Funktion verwenden, um zu einem bestimmten Vordergrundthread zu wechseln. Im folgenden Codebeispiel geben wir den Vordergrundthread an, indem die dispatcher-Warteschlange übergeben wird, die dem TextBlock zugeordnet ist (durch Zugriff auf die DispatcherQueue-Eigenschaft ). Die Implementierung von winrt::resume_foreground ruft für dieses Dispatcherwarteschlangenobjekt DispatcherQueue.TryEnqueue auf, um den Code auszuführen, der in der Koroutine darauf folgt.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    // Switch to the foreground thread associated with textblock.
    co_await winrt::resume_foreground(textblock.DispatcherQueue());

    textblock.Text(L"Done!"); // Guaranteed to work.
}

Die Winrt::resume_foreground-Funktion verwendet einen optionalen Prioritätsparameter. Wenn Sie diesen Parameter verwenden, ist das oben gezeigte Muster geeignet. Wenn nicht, können Sie co_await winrt::resume_foreground(someDispatcherObject); einfach in co_await someDispatcherObject; vereinfachen.

Ausführungskontexte, Fortsetzung und Wechsel in einer Coroutine

Allgemein gesprochen kann nach einem Unterbrechungspunkt in einer Coroutine der ursprüngliche Ausführungsthread nicht mehr vorhanden sein, und die Ausführung kann auf einem beliebigen Thread fortgesetzt werden (mit anderen Worten: Jeder Thread kann für den asynchronen Vorgang die Methode Completed aufrufen).

Wenn Sie jedoch co_await einen der vier asynchronen Vorgangstypen der Windows-Runtime (IAsyncXxx) verwenden, erfasst C++/WinRT den Aufrufkontext an dem Punkt, an dem Sie co_await. Außerdem wird sichergestellt, dass Sie sich immer noch in diesem Kontext befinden, wenn die Fortsetzung fortgesetzt wird. C++/WinRT tut dies, indem es prüft, ob Sie sich bereits im Aufrufkontext befinden, und andernfalls dorthin wechselt. Wenn Sie sich vor co_await in einem STA-Thread (Single-Threaded Apartment) befanden, befinden Sie sich danach wieder auf demselben Thread; wenn Sie sich vor co_await in einem MTA-Thread (Multi-Threaded Apartment) befanden, befinden Sie sich danach wieder auf einem solchen.

IAsyncAction ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;

    // The thread context at this point is captured...
    SyndicationFeed syndicationFeed{ co_await syndicationClient.RetrieveFeedAsync(rssFeedUri) };
    // ...and is restored at this point.
}

Der Grund, warum Sie sich auf dieses Verhalten verlassen können, besteht darin, dass C++/WinRT Code bereitstellt, um diese Windows-Runtime asynchronen Vorgangstypen an die C++-Coroutine-Sprachunterstützung anzupassen (diese Codeteile werden als Warteadapter bezeichnet). Die verbleibenden awaitbaren Typen in C++/WinRT sind einfach Threadpoolwrapper und/oder Hilfsklassen; sie werden also im Threadpool abgeschlossen.

using namespace std::chrono_literals;
IAsyncOperation<int> return_123_after_5s()
{
    // No matter what the thread context is at this point...
    co_await 5s;
    // ...we're on the thread pool at this point.
    co_return 123;
}

Wenn Sie co_await einen anderen Typ – auch innerhalb einer C++/WinRT-Coroutine-Implementierung – verwenden, stellt eine andere Bibliothek die Adapter bereit, und Sie müssen verstehen, was diese Adapter in Bezug auf Wiederaufnahme und Kontext tun.

Um Kontextwechsel auf ein Minimum zu reduzieren, können Sie einige der Techniken verwenden, die wir bereits in diesem Thema kennengelernt haben. Sehen wir uns einige Illustrationen dazu an. In diesem nächsten Pseudocodebeispiel zeigen wir die Gliederung eines Ereignishandlers, der eine Windows-Runtime-API aufruft, um ein Bild zu laden, auf einen Hintergrundthread fällt, um dieses Bild zu verarbeiten, und kehrt dann zum UI-Thread zurück, um das Bild in der Benutzeroberfläche anzuzeigen.

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    // Call StorageFile::OpenAsync to load an image file.

    // The call to OpenAsync occurred on a background thread, but C++/WinRT has restored us to the UI thread by this point.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Process the image.

    co_await winrt::resume_foreground(this->DispatcherQueue());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

Für dieses Szenario gibt es ein wenig Ineffizienz beim Aufruf von StorageFile::OpenAsync. Es ist ein erforderlicher Wechsel auf einen Hintergrundthread nötig (damit der Handler die Ausführung an den Aufrufer zurückgeben kann); bei der anschließenden Fortsetzung stellt C++/WinRT dann den Kontext des UI-Threads wieder her. In diesem Fall ist es jedoch nicht notwendig, sich im UI-Thread zu befinden, bis wir die Benutzeroberfläche aktualisieren möchten. Je mehr Windows-Runtime APIs wir vor unserem Aufruf von winrt::resume_background aufrufen, desto unnötiger sind die Kontextwechsel, die wir verursachen. Die Lösung besteht darin, vorher keine Windows-Runtime APIs aufzurufen. Verschieben Sie sie alle nach winrt::resume_background.

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Call StorageFile::OpenAsync to load an image file.

    // Process the image.

    co_await winrt::resume_foreground(this->DispatcherQueue());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

Wenn Sie etwas komplexeres tun möchten, können Sie Ihre eigenen Await-Adapter schreiben. Wenn Sie beispielsweise möchten, dass ein co_await in demselben Thread fortgesetzt wird, in dem die asynchrone Aktion abgeschlossen wird (sodass kein Kontextwechsel erfolgt), könnten Sie zunächst Await-Adapter schreiben, die den unten gezeigten ähneln.

Hinweis

Das folgende Codebeispiel wird nur für Bildungszwecke bereitgestellt; Es ist wichtig, dass Sie mit dem Verständnis der Funktionsweise von Await-Adaptern beginnen. Wenn Sie diese Technik in Ihrer eigenen Codebasis verwenden möchten, empfehlen wir Ihnen, ihre eigenen Await-Adapter-Struktur(n) zu entwickeln und zu testen. Sie können z. B. complete_on_any, complete_on_current und complete_on(Dispatcher)schreiben. Erwägen Sie außerdem, vorlagen zu erstellen, die den IAsyncXxx-Typ als Vorlagenparameter verwenden.

struct no_switch
{
    no_switch(Windows::Foundation::IAsyncAction const& async) : m_async(async)
    {
    }

    bool await_ready() const
    {
        return m_async.Status() == Windows::Foundation::AsyncStatus::Completed;
    }

    void await_suspend(std::experimental::coroutine_handle<> handle) const
    {
        m_async.Completed([handle](Windows::Foundation::IAsyncAction const& /* asyncInfo */, Windows::Foundation::AsyncStatus const& /* asyncStatus */)
        {
            handle();
        });
    }

    auto await_resume() const
    {
        return m_async.GetResults();
    }

private:
    Windows::Foundation::IAsyncAction const& m_async;
};

Um zu verstehen, wie Sie die no_switch-Await-Adapter verwenden, müssen Sie zunächst wissen, dass der C++-Compiler, wenn er auf einen co_await-Ausdruck trifft, nach Funktionen namens await_ready, await_suspend und await_resume sucht. Die C++/WinRT-Bibliothek stellt diese Funktionen bereit, sodass Sie standardmäßig ein angemessenes Verhalten erhalten, wie dies der Fall ist.

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

Wenn Sie die no_switch Await-Adapter verwenden möchten, ändern Sie einfach den Typ dieses co_await Ausdrucks von IAsyncXxx in no_switch, z. B.

IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);

Anstatt dann nach den drei await_xxx Funktionen zu suchen, die IAsyncXxx entsprechen, sucht der C++-Compiler nach Funktionen, die no_switch entsprechen.

Ein tieferer Einblick in winrt::resume_foreground

Ab C++/WinRT 2.0 setzt die winrt::resume_foreground-Funktion auch dann aus, wenn sie auf dem Dispatcherthread aufgerufen wird (in früheren Versionen konnte es in manchen Szenarien zu Deadlocks kommen, da sie nur ausgesetzt wurde, wenn sie nicht bereits auf dem Dispatcherthread aufgerufen wurde).

Das aktuelle Verhalten bedeutet, dass Sie sich darauf verlassen können, dass Stack-Unwinding und das erneute Einreihen in die Warteschlange erfolgen, und das ist wichtig für die Systemstabilität, insbesondere in systemnahem Code. In der letzten Codeauflistung im Abschnitt "Programmieren mit Threadaffinität" wird oben die Durchführung einer komplexen Berechnung in einem Hintergrundthread veranschaulicht und dann zum entsprechenden UI-Thread gewechselt, um die Benutzeroberfläche (UI) zu aktualisieren.

So sieht winrt::resume_foreground intern aus.

auto resume_foreground(...) noexcept
{
    struct awaitable
    {
        bool await_ready() const
        {
            return false; // Queue without waiting.
            // return m_dispatcher.HasThreadAccess(); // The C++/WinRT 1.0 implementation.
        }
        void await_resume() const {}
        void await_suspend(coroutine_handle<> handle) const { ... }
    };
    return awaitable{ ... };
};

Dieses aktuelle Verhalten im Vergleich zum vorherigen Verhalten entspricht dem Unterschied zwischen PostMessage und SendMessage in der Win32-Anwendungsentwicklung. PostMessage stellt die Arbeit in die Warteschlange und entspannt dann den Stapel, ohne auf den Abschluss der Arbeit zu warten. Die Stapelentspannung kann unerlässlich sein.

Die winrt::resume_foreground-Funktion unterstützte ursprünglich den CoreDispatcher (gebunden an einen CoreWindow), der vor Windows 10 eingeführt wurde. Verwenden Sie in WinUI 3- und Windows App SDK-Apps stattdessen "DispatcherQueue". Sie können einen DispatcherQueue für Ihre eigenen Zwecke erstellen. Betrachten Sie diese einfache Konsolenanwendung.

using namespace Windows::System;

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    auto controller{ DispatcherQueueController::CreateOnDedicatedThread() };
    RunAsync(controller.DispatcherQueue());
    getchar();
}

Im obigen Beispiel wird eine Warteschlange (in einem Controller enthalten) in einem privaten Thread erstellt und anschließend der Controller an die Coroutine übergeben. Die Coroutine kann die Warteschlange verwenden, um auf dem privaten Thread zu warten (angehalten und wieder aufgenommen zu werden). Eine weitere häufige Verwendung von DispatcherQueue besteht darin, eine Warteschlange im aktuellen UI-Thread für eine herkömmliche Desktop- oder Win32-App zu erstellen.

DispatcherQueueController CreateDispatcherQueueController()
{
    DispatcherQueueOptions options
    {
        sizeof(DispatcherQueueOptions),
        DQTYPE_THREAD_CURRENT,
        DQTAT_COM_STA
    };
 
    ABI::Windows::System::IDispatcherQueueController* ptr{};
    winrt::check_hresult(CreateDispatcherQueueController(options, &ptr));
    return { ptr, take_ownership_from_abi };
}

Dies veranschaulicht, wie Sie Win32-Funktionen in Ihre C++/WinRT-Projekte aufrufen und integrieren können, indem Sie einfach die Win32-Stil-CreateDispatcherQueueController-Funktion aufrufen, um den Controller zu erstellen, und dann den Besitz des resultierenden Warteschlangencontrollers als WinRT-Objekt an den Aufrufer übertragen. Genau so können Sie eine effiziente und nahtlose Warteschlange für Ihre vorhandene Winzold-Desktopanwendung im Petzold-Stil unterstützen.

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    Window window;
    auto controller{ CreateDispatcherQueueController() };
    RunAsync(controller.DispatcherQueue());
    MSG message;
 
    while (GetMessage(&message, nullptr, 0, 0))
    {
        DispatchMessage(&message);
    }
}

Oben beginnt die einfache Hauptfunktion mit dem Erstellen eines Fensters. Sie können sich vorstellen, dass dies eine Fensterklasse registriert und CreateWindow aufruft, um das Desktopfenster der obersten Ebene zu erstellen. Die Funktion CreateDispatcherQueueController wird dann aufgerufen, um den Queue-Controller zu erstellen, bevor eine Coroutine mit der Dispatcherwarteschlange aufgerufen wird, die diesem Controller zugeordnet ist. Anschließend wird eine herkömmliche Nachrichtenschleife gestartet, in der die Wiederaufnahme der Coroutine naturgemäß in diesem Thread erfolgt. Nachdem Sie dies getan haben, können Sie zur eleganten Welt von Coroutinen für Ihren asynchronen oder nachrichtenbasierten Workflow innerhalb Ihrer Anwendung zurückkehren.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ... // Begin on the calling thread...
 
    co_await winrt::resume_foreground(queue);
 
    ... // ...resume on the dispatcher thread.
}

Der Aufruf von winrt::resume_foreground wird immer in die Warteschlange eingereiht, und dann wird der Aufrufstapel abgewickelt. Optional können Sie auch die Priorität für die Wiederaufnahme festlegen.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
 
    ...
}

Oder verwenden Sie die Standardreihenfolge.

...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await queue;
 
    ...
}

Hinweis

Stellen Sie wie oben gezeigt sicher, dass Sie den Projektionsheader für den Namespace des Typs einbinden, den Sie mit co_await verwenden. Zum Beispiel Windows::System::DispatcherQueue oder Microsoft::UI::Dispatching::DispatcherQueue.

Oder in diesem Fall das Herunterfahren der Warteschlange zu erkennen und sauber damit umzugehen.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    if (co_await queue)
    {
        ... // Resume on dispatcher thread.
    }
    else
    {
        ... // Still on calling thread.
    }
}

Der Ausdruck co_await gibt true zurück, was darauf hinweist, dass die Wiederaufnahme auf dem Dispatcherthread erfolgt. Mit anderen Worten, dass die Warteschlange erfolgreich war. Umgekehrt gibt sie false zurück, um anzuzeigen, dass die Ausführung im aufrufenden Thread verbleibt, da der Controller der Warteschlange gerade heruntergefahren wird und keine Anforderungen an die Warteschlange mehr bedient.

Wenn Sie C++/WinRT mit Coroutinen kombinieren, stehen Ihnen also sehr leistungsfähige Möglichkeiten zur Verfügung, insbesondere bei der klassischen Desktopanwendungsentwicklung im Petzold-Stil.

Abbrechen eines asynchronen Vorgangs und Abbruchrückrufe

Mit den Features des Windows-Runtime für die asynchrone Programmierung können Sie eine asynchrone In-Flight-Aktion oder -Operation abbrechen. Hier ist ein Beispiel, das StorageFolder::GetFilesAsync aufruft, um eine potenziell große Sammlung von Dateien abzurufen, und es speichert das resultierende asynchrone Vorgangsobjekt in einem Datenmemm. Der Benutzer hat die Möglichkeit, den Vorgang abzubrechen.

// MainPage.xaml
...
<Button x:Name="workButton" Click="OnWork">Work</Button>
<Button x:Name="cancelButton" Click="OnCancel">Cancel</Button>
...

// MainPage.h
...
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Storage.Search.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Storage;
using namespace Windows::Storage::Search;
using namespace Microsoft::UI::Xaml;
...
struct MainPage : MainPageT<MainPage>
{
    MainPage()
    {
        InitializeComponent();
    }

    IAsyncAction OnWork(IInspectable /* sender */, RoutedEventArgs /* args */)
    {
        workButton().Content(winrt::box_value(L"Working..."));

        // Enable the Pictures Library capability in the app manifest file.
        StorageFolder picturesLibrary{ KnownFolders::PicturesLibrary() };

        m_async = picturesLibrary.GetFilesAsync(CommonFileQuery::OrderByDate, 0, 1000);

        IVectorView<StorageFile> filesInFolder{ co_await m_async };

        workButton().Content(box_value(L"Done!"));

        // Process the files in some way.
    }

    void OnCancel(IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
    {
        if (m_async.Status() != AsyncStatus::Completed)
        {
            m_async.Cancel();
            workButton().Content(winrt::box_value(L"Canceled"));
        }
    }

private:
    IAsyncOperation<::IVectorView<StorageFile>> m_async;
};
...

Für die Implementierungsseite des Abbruchs beginnen wir mit einem einfachen Beispiel.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction ImplicitCancelationAsync()
{
    while (true)
    {
        std::cout << "ImplicitCancelationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto implicit_cancelation{ ImplicitCancelationAsync() };
    co_await 3s;
    implicit_cancelation.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

Wenn Sie das obige Beispiel ausführen, werden Sie sehen, dass ImplicitCancelationAsync drei Sekunden lang einmal pro Sekunde eine Meldung ausgibt und danach infolge des Abbruchs automatisch beendet wird. Dies funktioniert, da beim Auftreten eines co_await Ausdrucks ein Coroutine überprüft, ob er abgebrochen wurde. Falls dies der Fall ist, wird der Vorgang sofort abgebrochen; andernfalls wird er wie gewohnt ausgesetzt.

Der Abbruch kann natürlich auch erfolgen, während die Coroutine suspendiert ist. Erst wenn die Coroutine wieder aufgenommen wird oder auf ein weiteres co_await trifft, wird auf Abbruch geprüft. Das Problem besteht in einer potenziell zu groben Granularität der Latenz beim Reagieren auf eine Abbruchanforderung.

Eine weitere Möglichkeit besteht also darin, die Abbruchsmeldung innerhalb Ihres Coroutine explizit abzufragen. Aktualisieren Sie das obige Beispiel mit dem Code in der nachstehenden Auflistung. In diesem neuen Beispiel ruft ExplicitCancelationAsync das von der winrt::get_cancellation_token-Funktion zurückgegebene Objekt ab und überprüft regelmäßig, ob die Coroutine abgebrochen wurde. Solange sie nicht abgebrochen wird, läuft die Coroutine endlos weiter; sobald sie abgebrochen wird, enden die Schleife und die Funktion normal. Das Ergebnis ist identisch mit dem vorherigen Beispiel, aber hier erfolgt das Beenden explizit und unter Kontrolle.

IAsyncAction ExplicitCancelationAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };

    while (!cancelation_token())
    {
        std::cout << "ExplicitCancelationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto explicit_cancelation{ ExplicitCancelationAsync() };
    co_await 3s;
    explicit_cancelation.Cancel();
}
...

Das Warten auf winrt::get_cancellation_token liefert ein Abbruchtoken, das die IAsyncAction kennt, die die Koroutine in Ihrem Auftrag erzeugt. Sie können den Funktionsaufrufoperator auf diesem Token verwenden, um den Abbruchstatus abzufragen – also im Wesentlichen ein regelmäßiges Prüfen auf Abbruch. Wenn Sie einen rechenintensiven Vorgang ausführen oder eine große Sammlung durchlaufen, dann ist dies eine sinnvolle Methode.

Registrieren eines Abbruchrückrufs

Der Abbruch in der Windows-Runtime wird nicht automatisch an andere asynchrone Objekte weitergegeben. Aber Sie können – eingeführt in Version 10.0.17763.0 (Windows 10, Version 1809) des Windows SDK – einen Abbruch-Callback registrieren. Dies ist ein vorbeugender Hook, mit dessen Hilfe eine Abbruchanforderung weitergegeben werden kann, und er ermöglicht die Integration in vorhandene Nebenläufigkeitsbibliotheken.

In diesem nächsten Codebeispiel führt NestedCoroutineAsync die Arbeit durch, weist jedoch keine spezielle Abbruchlogik darin auf. CancelationPropagatorAsync ist im Wesentlichen ein Wrapper für die verschachtelte Coroutine; der Wrapper leitet Abbruchanforderungen vorsorglich weiter.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction NestedCoroutineAsync()
{
    while (true)
    {
        std::cout << "NestedCoroutineAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction CancelationPropagatorAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };
    auto nested_coroutine{ NestedCoroutineAsync() };

    cancelation_token.callback([=]
    {
        nested_coroutine.Cancel();
    });

    co_await nested_coroutine;
}

IAsyncAction MainCoroutineAsync()
{
    auto cancelation_propagator{ CancelationPropagatorAsync() };
    co_await 3s;
    cancelation_propagator.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

CancelationPropagatorAsync registriert eine Lambda-Funktion als eigenen Rückruf für den Abbruch und wartet dann asynchron (die Ausführung wird ausgesetzt), bis die verschachtelte Operation abgeschlossen ist. Wenn oder falls CancellationPropagatorAsync abgebrochen wird, wird die Abbruchanforderung an die geschachtelte Coroutine weitergeleitet. Es ist nicht erforderlich, die Absagen abzufragen; die Kündigung wird auch nicht unbegrenzt blockiert. Dieser Mechanismus ist flexibel genug, um ihn für die Interoperabilität mit einer Coroutine- oder Parallelitätsbibliothek zu verwenden, die nichts von C++/WinRT kennt.

Berichterstellungsfortschritt

Wenn Ihre Coroutine entweder IAsyncActionWithProgress oder IAsyncOperationWithProgress zurückgibt, können Sie das von der Funktion winrt::get_progress_token zurückgegebene Objekt abrufen und es verwenden, um den Fortschritt an einen Fortschrittshandler zu melden. Hier ist ein Codebeispiel.

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncOperationWithProgress<double, double> CalcPiTo5DPs()
{
    auto progress{ co_await winrt::get_progress_token() };

    co_await 1s;
    double pi_so_far{ 3.1 };
    progress.set_result(pi_so_far);
    progress(0.2);

    co_await 1s;
    pi_so_far += 4.e-2;
    progress.set_result(pi_so_far);
    progress(0.4);

    co_await 1s;
    pi_so_far += 1.e-3;
    progress.set_result(pi_so_far);
    progress(0.6);

    co_await 1s;
    pi_so_far += 5.e-4;
    progress.set_result(pi_so_far);
    progress(0.8);

    co_await 1s;
    pi_so_far += 9.e-5;
    progress.set_result(pi_so_far);
    progress(1.0);

    co_return pi_so_far;
}

IAsyncAction DoMath()
{
    auto async_op_with_progress{ CalcPiTo5DPs() };
    async_op_with_progress.Progress([](auto const& sender, double progress)
    {
        std::wcout << L"CalcPiTo5DPs() reports progress: " << progress << L". "
                   << L"Value so far: " << sender.GetResults() << std::endl;
    });
    double pi{ co_await async_op_with_progress };
    std::wcout << L"CalcPiTo5DPs() is complete !" << std::endl;
    std::wcout << L"Pi is approx.: " << pi << std::endl;
}

int main()
{
    winrt::init_apartment();
    DoMath().get();
}

Um den Fortschritt zu melden, rufen Sie das Fortschrittstoken mit dem Fortschrittswert als Argument auf. Um ein vorläufiges Ergebnis festzulegen, verwenden Sie die Methode set_result() am Fortschrittstoken.

Hinweis

Für die Berichterstellung von vorläufigen Ergebnissen ist C++/WinRT, Version 2.0.210309.3 oder höher, erforderlich.

Im obigen Beispiel wird für jeden Fortschrittsbericht ein vorläufiges Ergebnis festgelegt. Sie können sich jederzeit dafür entscheiden, vorläufige Ergebnisse zu melden – oder auch gar nicht. Sie muss nicht mit einem Fortschrittsbericht gekoppelt werden.

Hinweis

Es ist nicht richtig, mehr als einen Abschlusshandler für eine asynchrone Aktion oder einen Vorgang zu implementieren. Sie können entweder eine einzelne Stellvertretung für das abgeschlossene Ereignis haben, oder Sie können co_await es. Wenn Sie beides haben, schlägt die zweite fehl. Eine der folgenden beiden Arten von Abschlusshandlern ist geeignet; nicht beides für dasselbe asynchrone Objekt.

auto async_op_with_progress{ CalcPiTo5DPs() };
async_op_with_progress.Completed([](auto const& sender, AsyncStatus /* status */)
{
    double pi{ sender.GetResults() };
});
auto async_op_with_progress{ CalcPiTo5DPs() };
double pi{ co_await async_op_with_progress };

Weitere Informationen zu Abschlusshandlern finden Sie unter Delegattypen für asynchrone Aktionen und Vorgänge.

Feuer und Vergessen

Manchmal haben Sie eine Aufgabe, die gleichzeitig mit anderen Arbeiten ausgeführt werden kann, und Sie müssen nicht warten, bis diese Aufgabe abgeschlossen ist (keine andere Arbeit hängt davon ab), noch müssen Sie einen Wert zurückgeben. In diesem Fall können Sie die Aufgabe auslöschen und vergessen. Sie können dies tun, indem Sie eine Coroutine schreiben, deren Rückgabetyp winrt::fire_and_forget ist (anstelle eines der asynchronen Vorgangstypen der Windows-Runtime oder concurrency::task).

// main.cpp
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace std::chrono_literals;

winrt::fire_and_forget CompleteInFiveSeconds()
{
    co_await 5s;
}

int main()
{
    winrt::init_apartment();
    CompleteInFiveSeconds();
    // Do other work here.
}

winrt::fire_and_forget ist auch als Rückgabetyp Ihres Ereignishandlers nützlich, wenn Sie darin asynchrone Operationen ausführen müssen. Hier ist ein Beispiel (siehe auch starke und schwache Verweise in C++/WinRT).

winrt::fire_and_forget MyClass::MyMediaBinder_OnBinding(MediaBinder const&, MediaBindingEventArgs args)
{
    auto lifetime{ get_strong() }; // Prevent *this* from prematurely being destructed.
    auto ensure_completion{ unique_deferral(args.GetDeferral()) }; // Take a deferral, and ensure that we complete it.

    auto file{ co_await StorageFile::GetFileFromApplicationUriAsync(Uri(L"ms-appx:///video_file.mp4")) };
    args.SetStorageFile(file);

    // The destructor of unique_deferral completes the deferral here.
}

Das erste Argument (der Absender) bleibt unbenannt, da wir es nie verwenden. Aus diesem Grund können wir es bedenkenlos als Referenz beibehalten. Beachten Sie jedoch, dass args als Wert übergeben wird. Siehe oben den Abschnitt "Parameterübergabe ".

Warten auf ein Kernelhandle

C++/WinRT stellt eine winrt::resume_on_signal-Funktion bereit, mit der Sie anhalten können, bis ein Kernelereignis signalisiert wird. Sie sind dafür verantwortlich, sicherzustellen, dass das Handle gültig bleibt, bis co_await resume_on_signal(h) zurückkehrt. resume_on_signal selbst kann das nicht leisten, da Sie das Handle möglicherweise bereits verloren haben, bevor resume_on_signal überhaupt startet, wie in diesem ersten Beispiel.

IAsyncAction Async(HANDLE event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle is not valid here.
}

Das eingehende HANDLE ist nur so lange gültig, bis die Funktion zurückkehrt, und diese Funktion (die eine Coroutine ist) kehrt am ersten Unterbrechungspunkt zurück (in diesem Fall dem ersten co_await). Während sie auf DoWorkAsync warten, wurde das Steuerelement an den Aufrufer zurückgegeben, der aufrufende Frame ist außerhalb des Gültigkeitsbereichs gegangen, und Sie wissen nicht mehr, ob das Handle gültig ist, wenn Ihr Coroutine fortgesetzt wird.

Technisch gesehen empfängt unsere Coroutine ihre Parameter als Wert, wie es sein sollte (siehe oben unter Parameterübergabe). Aber in diesem Fall müssen wir einen Schritt weitergehen, damit wir dem Geist dieser Anleitung folgen (und nicht nur dem Brief). Wir müssen zusammen mit dem Handle eine starke Referenz (also die Eigentümerschaft) übergeben. Gehen Sie dazu wie folgt vor:

IAsyncAction Async(winrt::handle event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle *is* valid here.
}

Die Übergabe eines winrt::handle als Wert verleiht ihm Besitzersemantik, wodurch sichergestellt wird, dass das Kernel-Handle während der Lebensdauer der Coroutine gültig bleibt.

Hier erfahren Sie, wie Sie diese Coroutine nennen können.

namespace
{
    winrt::handle duplicate(winrt::handle const& other, DWORD access)
    {
        winrt::handle result;
        if (other)
        {
            winrt::check_bool(::DuplicateHandle(::GetCurrentProcess(),
		        other.get(), ::GetCurrentProcess(), result.put(), access, FALSE, 0));
        }
        return result;
    }

    winrt::handle make_manual_reset_event(bool initialState = false)
    {
        winrt::handle event{ ::CreateEvent(nullptr, true, initialState, nullptr) };
        winrt::check_bool(static_cast<bool>(event));
        return event;
    }
}

IAsyncAction SampleCaller()
{
    handle event{ make_manual_reset_event() };
    auto async{ Async(duplicate(event)) };

    ::SetEvent(event.get());
    event.close(); // Our handle is closed, but Async still has a valid handle.

    co_await async; // Will wake up when *event* is signaled.
}

Sie können einen Timeout-Wert für resume_on_signal angeben, wie in diesem Beispiel.

winrt::handle event = ...

if (co_await winrt::resume_on_signal(event.get(), std::literals::2s))
{
    puts("signaled");
}
else
{
    puts("timed out");
}

Asynchrone Timeouts einfach gemacht

C++/WinRT setzt stark auf C++-Koroutinen. Ihre Auswirkung auf das Schreiben von Parallelitätscode ist transformational. In diesem Abschnitt werden Fälle erläutert, in denen die Details der asynchronen Verarbeitung keine Rolle spielen und Sie einfach nur sofort das Ergebnis erhalten möchten. Aus diesem Grund verfügt die C++/WinRT-Implementierung der IAsyncAction-Windows-Runtime asynchronen Vorgangsschnittstelle über eine Get-Funktion, ähnlich wie die von std::future bereitgestellt.

using namespace winrt::Windows::Foundation;
int main()
{
    IAsyncAction async = ...
    async.get();
    puts("Done!");
}

Die get-Funktion blockiert auf unbestimmte Zeit, bis das asynchrone Objekt abgeschlossen ist. Asynchrone Objekte neigen dazu, sehr kurzlebig zu sein, daher ist dies oft alles, was Sie brauchen.

Aber es gibt Fälle, in denen das nicht ausreicht, und Sie müssen die Wartezeit aufgeben, nachdem einige Zeit verstrichen ist. Das Schreiben dieses Codes war dank der von der Windows-Runtime bereitgestellten Bausteine immer möglich. Aber jetzt macht C++/WinRT es viel einfacher, indem es die Funktion wait_for bereitstellt. Es wird auch auf IAsyncAction implementiert, und auch hier ist es ähnlich wie bei "std::future".

using namespace std::chrono_literals;
int main()
{
    IAsyncAction async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        puts("done");
    }
}

Hinweis

wait_for verwendet in der Schnittstelle std::chrono::duration, ist aber auf einen Bereich beschränkt, der kleiner ist als der von std::chrono::duration bereitgestellte (ungefähr 49,7 Tage).

Der wait_for in diesem nächsten Beispiel wartet etwa fünf Sekunden und überprüft dann den Abschluss. Wenn der Vergleich günstig ist, wissen Sie, dass das asynchrone Objekt erfolgreich abgeschlossen wurde und Sie fertig sind. Wenn Sie auf ein Ergebnis warten, können Sie dies einfach mit einem Aufruf der GetResults-Methode befolgen, um das Ergebnis abzurufen.

Hinweis

wait_for und get schließen sich gegenseitig aus (Sie können nicht beide aufrufen). Sie zählen jeweils als Wartender, und asynchrone Aktionen/Vorgänge der Windows-Runtime unterstützen nur einen einzelnen Wartenden.

int main()
{
    IAsyncOperation<int> async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        printf("result %d\n", async.GetResults());
    }
}

Da das asynchrone Objekt dann abgeschlossen wurde, gibt die GetResults-Methode das Ergebnis sofort ohne weitere Wartezeit zurück. Wie Sie sehen können, gibt wait_for den Status des asynchronen Objekts zurück. Sie können es also wie folgt für feinkörnigere Kontrolle verwenden.

switch (async.wait_for(5s))
{
case AsyncStatus::Completed:
    printf("result %d\n", async.GetResults());
    break;
case AsyncStatus::Canceled:
    puts("canceled");
    break;
case AsyncStatus::Error:
    puts("failed");
    break;
case AsyncStatus::Started:
    puts("still running");
    break;
}
  • Denken Sie daran, dass AsyncStatus::Completed bedeutet, dass das asynchrone Objekt erfolgreich abgeschlossen wurde, und Sie können die GetResults-Methode aufrufen, um jedes Ergebnis abzurufen.
  • AsyncStatus::Canceled bedeutet, dass das asynchrone Objekt abgebrochen wurde. Ein Abbruch wird in der Regel vom Aufrufer angefordert, daher kommt es selten vor, mit diesem Zustand umzugehen. In der Regel wird ein abgebrochenes asynchrones Objekt einfach verworfen. Sie können die Methode GetResults aufrufen, um die Abbruchausnahme auf Wunsch erneut auszulösen.
  • AsyncStatus::Error bedeutet, dass das asynchrone Objekt auf irgendeine Weise fehlgeschlagen ist. Sie können die GetResults-Methode aufrufen, um die Ausnahme bei Bedarf erneut zu überschreiben.
  • AsyncStatus::Started bedeutet, dass das asynchrone Objekt weiterhin ausgeführt wird. Das asynchrone Muster der Windows-Runtime lässt weder mehrere Wartevorgänge noch mehrere Wartende zu. Das bedeutet, dass Sie wait_for nicht in einer Schleife aufrufen können. Wenn die Wartezeit effektiv abgelaufen ist, stehen Ihnen einige Optionen zur Verfügung. Sie können das Objekt verlassen, oder Sie können den Status abfragen, bevor Sie die GetResults-Methode aufrufen, um ein beliebiges Ergebnis abzurufen. Es ist jedoch am besten, das Objekt an diesem Punkt zu verwerfen.

Eine Alternative ist, nur auf Gestartet zu prüfen und GetResults die anderen Fälle behandeln zu lassen.

if (async.wait_for(5s) == AsyncStatus::Started)
{
    puts("timed out");
}
else
{
    // will throw appropriate exception if in canceled or error state
    auto results = async.GetResults();
}

Ein Array asynchron zurückgeben

Im Folgenden finden Sie ein Beispiel für MIDL 3.0, das den Fehler MIDL2025 erzeugt: [msg]syntax error [context]: expecting > or, near "[".

Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();

Der Grund dafür ist, dass es ungültig ist, ein Array als Parametertypargument für eine parametrisierte Schnittstelle zu verwenden. Daher benötigen wir eine weniger offensichtliche Methode, um das Ziel zu erreichen, ein Array asynchron aus einer Laufzeitklassenmethode zurück zu übergeben.

Sie können das Array, gekapselt in ein PropertyValue-Objekt, zurückgeben. Der aufrufende Code entpackt ihn dann. Hier ist ein Codebeispiel, das Sie testen können, indem Sie die SampleComponent-Laufzeitklasse zu einem Windows-Runtime Component (C++/WinRT)-Projekt hinzufügen und diese dann beispielsweise in einem Blank App, Packaged (WinUI 3 in Desktop)-Projekt verwenden.

// SampleComponent.idl
namespace MyComponentProject
{
    runtimeclass SampleComponent
    {
        Windows.Foundation.IAsyncOperation<IInspectable> RetrieveCollectionAsync();
    };
}

// SampleComponent.h
...
struct SampleComponent : SampleComponentT<SampleComponent>
{
    ...
    Windows::Foundation::IAsyncOperation<Windows::Foundation::IInspectable> RetrieveCollectionAsync()
    {
        co_return Windows::Foundation::PropertyValue::CreateInt32Array({ 99, 101 }); // Box an array into a PropertyValue.
    }
}
...

// SampleCoreApp.cpp
...
MyComponentProject::SampleComponent m_sample_component;
...
auto boxed_array{ co_await m_sample_component.RetrieveCollectionAsync() };
auto property_value{ boxed_array.as<winrt::Windows::Foundation::IPropertyValue>() };
winrt::com_array<int32_t> my_array;
property_value.GetInt32Array(my_array); // Unbox back into an array.
...

Wichtige APIs