Udostępnij za pomocą


Zaawansowana współbieżność i asynchronia z językiem C++/WinRT

W tym temacie opisano zaawansowane scenariusze ze współbieżnością i asynchronią w języku C++/WinRT.

Aby zapoznać się z wprowadzeniem do tematu, najpierw przeczytaj Współbieżność i Operacje Asynchroniczne.

Przekazywanie zadań do puli wątków systemu Windows

Coroutine jest funkcją podobną do każdej innej w tym, że obiekt wywołujący jest blokowany, dopóki funkcja nie zwróci do niego wykonania. A pierwszą okazją do powrotu korutyny jest pierwszy co_await, co_returnlub co_yield.

Dlatego zanim wykonasz pracę związaną z obliczeniami w kohroutynie, musisz przywrócić wykonanie do wywołującego (innymi słowy, wprowadzić punkt zawieszenia), aby obiekt wywołujący nie był blokowany. Jeśli jeszcze tego nie robisz poprzez co_await-anie innej operacji, możesz użyć co_await funkcji winrt::resume_background. Spowoduje to zwrócenie kontroli do elementu wywołującego, a następnie natychmiastowe wznowienie wykonywania w wątku puli wątków.

Pula wątków wykorzystywana w implementacji to niskopoziomowa pula wątków systemu Windows, oznaczona jako , więc działa z optymalną wydajnością.

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

Programowanie z uwzględnieniem koligacji wątków

Ten scenariusz rozszerza się na poprzedni. Odciążasz część pracy w puli wątków, ale następnie chcesz wyświetlić postęp w interfejsie użytkownika.

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

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

Powyższy kod zgłasza wyjątek winrt::hresult_wrong_thread, ponieważ TextBlock musi być zaktualizowany z wątku, który go stworzył, czyli z wątku interfejsu użytkownika. Jednym z rozwiązań jest przechwycenie kontekstu wątku, w którym została pierwotnie wywołana nasza kohroutyna. W tym celu utwórz wystąpienie obiektu winrt::apartment_context, wykonaj pracę w tle, a następnie co_awaitapartment_context, aby przełączyć się z powrotem do kontekstu wywołującego.

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

Tak długo, jak coroutine powyżej jest wywoływana z wątku interfejsu użytkownika, który utworzył TextBlock, to ta technika działa. W aplikacji będzie wiele przypadków, w których masz pewność, że tak.

W przypadku bardziej ogólnego rozwiązania do aktualizowania interfejsu użytkownika, który obejmuje przypadki, w których nie masz pewności co do wątku wywołującego, można co_awaitfunkcji winrt::resume_foreground, aby przełączyć się do określonego wątku pierwszego planu. W poniższym przykładzie kodu określamy wątek główny, przekazując obiekt dyspozytora skojarzony z TextBlock (poprzez dostęp do jego właściwości Dispatcher). Implementacja winrt::resume_foreground wywołuje CoreDispatcher.RunAsync na tym obiekcie dispatcher w celu wykonania polecenia wykonywanego po nim w korutynie.

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.Dispatcher());

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

Funkcja winrt::resume_foreground przyjmuje opcjonalny parametr priorytetu. Jeśli używasz tego parametru, wzorzec pokazany powyżej jest odpowiedni. Jeśli nie, możesz uprościć co_await winrt::resume_foreground(someDispatcherObject); do samego co_await someDispatcherObject;.

Konteksty wykonywania, wznawianie i przełączanie w korutynie

Mówiąc ogólnie, po punkcie zawieszenia w korutynie, oryginalny wątek wykonawczy może zniknąć i wznowienie może wystąpić w dowolnym wątku (innymi słowy, każdy wątek może wywołać metodę zakończenia dla operacji asynchronicznej).

Jeśli jednak co_await dowolnego z czterech typów operacji asynchronicznych środowiska uruchomieniowego systemu Windows (IAsyncXxx), język C++/WinRT przechwytuje kontekst wywołania w momencie co_await. Gwarantuje to, że nadal jesteś w tym kontekście, gdy kontynuacja zostanie wznowiona. C++/WinRT robi to, sprawdzając, czy jesteś już w kontekście wywołania i, jeśli nie, przełącza się do niego. Jeśli byłeś na wątku jednowątkowym (STA) przed co_await, to będziesz na tym samym później; jeśli byłeś na wątku wielowątkowym (MTA) przed co_await, to będziesz na takim samym później.

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

Powodem, dla którego można polegać na tym zachowaniu, jest to, że język C++/WinRT udostępnia kod umożliwiający dostosowanie tych typów operacji asynchronicznych środowiska uruchomieniowego systemu Windows do obsługi języka c++ coroutine (te fragmenty kodu są nazywane adapterami oczekiwania). Pozostałe typy oczekiwane w C++/WinRT to po prostu otoki puli wątków i/lub pomocnicze narzędzia; dlatego są one realizowane w puli wątków.

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

Jeśli co_await jakiś inny typ — nawet w ramach implementacji coroutine w C++/WinRT — to inna biblioteka dostarcza adaptery, i musisz zrozumieć, jak te adaptery działają pod względem wznawiania i kontekstów.

Aby zachować przełączniki kontekstowe do minimum, możesz użyć niektórych technik, które już widzieliśmy w tym temacie. Zobaczmy kilka ilustracji jak to zrobić. W następnym przykładzie pseudo-kodu przedstawiamy konspekt obsługi zdarzeń, która wywołuje interfejs API środowiska uruchomieniowego systemu Windows w celu załadowania obrazu, przechodzi do wątku tła w celu przetworzenia tego obrazu, a następnie wraca do wątku interfejsu użytkownika, aby wyświetlić obraz.

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->Dispatcher());

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

    // Display the image in the UI.
}

W tym scenariuszu istnieje trochę nieefektywności wokół wywołania metody StorageFile::OpenAsync. Konieczne jest przełączenie kontekstu do wątku w tle (aby program obsługi mógł powrócić do wywołującego), po wznowieniu, po którym C++/WinRT przywraca kontekst wątku interfejsu użytkownika. Jednak w tym przypadku nie trzeba być w wątku interfejsu użytkownika, dopóki nie zostanie zaktualizowany interfejs użytkownika. Im więcej interfejsów API środowiska uruchomieniowego systemu Windows wywołujemy przed naszym wywołaniem do winrt::resume_background, tym więcej niepotrzebnych przełączeń kontekstowych doświadczamy. Rozwiązaniem nie jest wywoływanie żadnych interfejsów API środowiska uruchomieniowego systemu Windows przed tym czasie. Przenieś je wszystkie po 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->Dispatcher());

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

    // Display the image in the UI.
}

Jeżeli chcesz zrobić coś bardziej zaawansowanego, możesz napisać własne adaptery await. Na przykład, jeśli chcesz, aby co_await wznowiło działanie w tym samym wątku, na którym kończy się akcja asynchroniczna (więc nie ma przełączania kontekstu), możesz zacząć od zapisu adapterów await w sposób podobny do pokazanych poniżej.

Uwaga / Notatka

Poniższy przykład kodu jest dostarczany wyłącznie w celach edukacyjnych, aby pomóc Ci rozpocząć zrozumienie działania adapterów await. Jeśli chcesz użyć tej techniki we własnej bazie kodu, zalecamy opracowanie i przetestowanie własnych struktur adapterów await. Można na przykład napisać complete_on_any, complete_on_currenti complete_on(dyspozytor). Rozważ również utworzenie szablonów, które przyjmują typ IAsyncXxx jako parametr szablonu.

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

Aby zrozumieć, jak używać adapterów no_switch await, najpierw musisz wiedzieć, że gdy kompilator języka C++ napotka wyrażenie co_await, szuka funkcji o nazwach await_ready, await_suspendi await_resume. Biblioteka C++/WinRT udostępnia te funkcje, dzięki czemu domyślnie uzyskujesz rozsądne zachowanie.

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

Aby użyć adapterów no_switch await, po prostu zmień typ tego wyrażenia co_await z IAsyncXxx na no_switch, tak jak to pokazano poniżej.

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

Następnie zamiast wyszukiwać trzy funkcje await_xxx zgodne z IAsyncXxx, kompilator języka C++ szuka funkcji pasujących do no_switch.

Dokładniejsze omówienie winrt::resume_foreground

Od momentu wprowadzenia C++/WinRT 2.0, funkcja winrt::resume_foreground zatrzymuje się nawet wtedy, gdy jest wywoływana z wątku dyspozytora (w poprzednich wersjach mogła ona prowadzić do zakleszczeń w pewnych scenariuszach, ponieważ zatrzymywała się tylko wtedy, gdy nie była już na wątku dyspozytora).

Bieżące zachowanie oznacza, że można polegać na tym, że odwijanie stosu i ponowne kolejkowanie mają miejsce, co jest ważne dla stabilności systemu, zwłaszcza w kodzie systemów na niskim poziomie. Ostatni listing kodu w sekcji Programowanie z uwzględnieniem koligacji wątkupowyżej, ilustruje wykonywanie pewnych złożonych obliczeń w wątku w tle, a następnie przełączanie na odpowiedni wątek interfejsu użytkownika w celu jego zaktualizowania.

Oto jak winrt::resume_foreground wygląda wewnętrznie.

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{ ... };
};

Bieżące zachowanie, w przeciwieństwie do poprzedniego, jest analogiczne do różnicy między PostMessage a SendMessage w tworzeniu aplikacji Win32. postMessage kolejkuje pracę, a następnie odwija stos bez oczekiwania na ukończenie pracy. Odwijanie stosu może być niezbędne.

Funkcja winrt::resume_foreground początkowo obsługiwała tylko CoreDispatcher (powiązana z CoreWindow), która została wprowadzona przed systemem Windows 10. Od tego czasu wprowadziliśmy bardziej elastyczny i wydajny dyspozytor: DispatcherQueue. Możesz utworzyć DispatcherQueue dla własnych celów. Rozważmy tę prostą aplikację konsolową.

using namespace Windows::System;

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

Powyższy przykład tworzy kolejkę (zawartą w kontrolerze) w wątku prywatnym, a następnie przekazuje kontroler do kohroutyny. Coroutine może użyć kolejki do oczekiwania (wstrzymywanie i wznawianie) w wątku prywatnym. Innym typowym zastosowaniem DispatcherQueue jest utworzenie kolejki w bieżącym wątku interfejsu użytkownika dla tradycyjnej aplikacji desktopowej lub aplikacji Win32.

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

Ilustruje to, jak można wywoływać i dołączać funkcje Win32 do projektów C++/WinRT, po prostu wywołując funkcję w stylu Win32 CreateDispatcherQueueController, aby utworzyć kontroler, a następnie przenieść własność otrzymanego kontrolera kolejki do wywołującego jako obiektu WinRT. Jest to również sposób, w jaki można obsługiwać wydajne i bezproblemowe kolejkowanie w istniejącej aplikacji klasycznej Win32 w stylu Petzold.

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

Powyżej prosta funkcja główna rozpoczyna się od utworzenia okna. Można sobie wyobrazić, że spowoduje to zarejestrowanie klasy okna i wywołanie metody CreateWindow w celu utworzenia okna pulpitu najwyższego poziomu. funkcja CreateDispatcherQueueController jest następnie wywoływana w celu utworzenia kontrolera kolejki przed wywołaniem pewnej kohroutyny z kolejką dyspozytora należącego do tego kontrolera. Następnie wchodzimy w tradycyjną pętlę wiadomości, gdzie wznowienie korutyny naturalnie następuje w tym wątku. Po wykonaniu tej czynności możesz wrócić do eleganckiego świata kohroutyn dla przepływu pracy asynchronicznego lub opartego na komunikatach w aplikacji.

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

Wywołanie winrt::resume_foreground zawsze będzie kolejki, a następnie odwij stos. Opcjonalnie można również ustawić priorytet wznowienia.

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

Możesz też użyć domyślnej kolejności kolejkowania.

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

Uwaga / Notatka

Jak pokazano powyżej, pamiętaj, aby uwzględnić nagłówek projekcji dla przestrzeni nazw typu, który jest co_await-owany. Na przykład Windows::UI::Core::CoreDispatcher, Windows::System::DispatcherQueuelub Microsoft::UI::Dispatching::DispatcherQueue.

Lub w tym przypadku wykrywanie zamknięcia kolejki i jego łagodne obsługiwanie.

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

Wyrażenie co_await zwraca true, co wskazuje, że wznowienie nastąpi w wątku dyspozytora. Innymi słowy, kolejkowanie zakończyło się pomyślnie. Z drugiej strony zwraca false, aby wskazać, że wykonywanie pozostaje w wątku wywołującym, ponieważ kontroler kolejki jest zamykany i nie obsługuje już żądań kolejki.

Masz więc dużą moc na wyciągnięcie ręki podczas łączenia języka C++/WinRT z coroutines; a zwłaszcza podczas tworzenia aplikacji klasycznych w stylu old-school Petzold.

Anulowanie operacji asynchronicznej i anulowanie wywołań zwrotnych

Funkcje środowiska uruchomieniowego systemu Windows dla programowania asynchronicznego umożliwiają anulowanie akcji lub operacji asynchronicznej w locie. Oto przykład, który wywołuje StorageFolder::GetFilesAsync w celu pobrania potencjalnie dużej kolekcji plików i przechowuje wynikowy obiekt operacji asynchronicznej w elemencie członkowskim danych. Użytkownik ma możliwość anulowania operacji.

// 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 Windows::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;
};
...

Po stronie implementacji anulowania zacznijmy od prostego przykładu.

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

Jeśli uruchomisz powyższy przykład, zobaczysz, jak ImplicitCancelationAsync drukuje jeden komunikat na sekundę przez trzy sekundy, po czym zostanie automatycznie zakończony w wyniku anulowania. To działa, ponieważ po napotkaniu wyrażenia co_await, korutyna (coroutine) sprawdza, czy została anulowana. Jeśli tak, to następuje zwarcie; a jeśli nie, to zawiesza się jak zwykle.

Anulowanie może oczywiście nastąpić, gdy korutyna jest zawieszona. Tylko wtedy, gdy korutyna zostanie wznowiona lub trafi do innego co_await, będzie sprawdzać anulowanie. Problem polega na potencjalnie zbyt dużej opóźnionej reakcji na anulowanie.

Kolejną opcją jest jawne sondowania pod kątem anulowania z poziomu kohroutyny. Zaktualizuj powyższy przykład przy użyciu kodu na poniższej liście. W tym nowym przykładzie funkcja ExplicitCancelationAsync pobiera obiekt zwrócony przez funkcję winrt::get_cancellation_token i używa go do okresowego sprawdzania, czy coroutine została anulowana. O ile nie zostanie anulowana, korutyna działa w nieskończoność; gdy zostanie anulowana, pętla i funkcja zakończą się normalnie. Wynik jest taki sam jak w poprzednim przykładzie, ale tutaj wyjście odbywa się jawnie i pod kontrolą.

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

Oczekiwanie na winrt::get_cancellation_token pobiera token anulowania, biorąc pod uwagę IAsyncAction, którą coroutine produkuje w Twoim imieniu. Możesz użyć operatora wywołania funkcji na tym tokenie, aby wykonać zapytanie o stan anulowania — zasadniczo sondowanie pod kątem anulowania. Jeśli wykonujesz operację związaną z obliczeniami lub wykonujesz iterację za pośrednictwem dużej kolekcji, jest to rozsądna technika.

Zarejestruj procedurę zwrotną anulowania

Anulowanie środowiska uruchomieniowego systemu Windows nie rozprzestrzenia się automatycznie na inne obiekty asynchroniczne. Jednak w wersji 10.0.17763.0 (Windows 10, wersja 1809) zestawu Windows SDK wprowadzono możliwość zarejestrowania wywołania zwrotnego anulowania. Jest to haczyk prewencyjny, który umożliwia propagację anulowania i ułatwia integrację z istniejącymi bibliotekami współbieżności.

W następnym przykładzie kodu nestedCoroutineAsync wykonuje pracę, ale nie ma w nim specjalnej logiki anulowania. CancelationPropagatorAsync jest zasadniczo opakowaniem zagnieżdżonej korutyny; opakowanie przesyła anulowanie z wyprzedzeniem.

// 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 rejestruje funkcję lambda dla własnego wywołania zwrotnego anulowania, a następnie oczekuje (zawiesza wykonywanie) do momentu zakończenia zagnieżdżonej pracy. Jeśli lub jeśli CancelPropagatorAsync zostanie anulowana, propaguje anulowanie do zagnieżdżonej kohroutyny. Nie ma potrzeby sondowania pod kątem anulowania; anulowanie nie jest blokowane na czas nieokreślony. Mechanizm ten jest wystarczająco elastyczny, aby można go było używać do interoperacyjności z biblioteką współprogramów lub współbieżności, które nie znają języka C++/WinRT.

Raportowanie postępów

Jeśli coroutine zwraca wartość IAsyncActionWithProgresslub IAsyncOperationWithProgress, możesz pobrać obiekt zwrócony przez funkcję winrt::get_progress_token i użyć go do zgłaszania postępu do procedury obsługi postępu. Oto przykład kodu.

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

Aby zgłosić postęp, wywołaj token postępu z wartością postępu jako argumentem. Aby ustawić wynik tymczasowy, użyj metody set_result() na tokenie postępu.

Uwaga / Notatka

Raportowanie wyników tymczasowych wymaga języka C++/WinRT w wersji 2.0.210309.3 lub nowszej.

W powyższym przykładzie zdecydowano się ustawić tymczasowy wynik dla każdego raportu postępu. Możesz w dowolnym momencie zgłaszać tymczasowe wyniki, jeśli w ogóle. Nie trzeba go stosować w połączeniu z raportem o postępach.

Uwaga / Notatka

Nie jest poprawne zaimplementowanie więcej niż jednej procedury kończącej dla asynchronicznej akcji lub operacji. Możesz mieć pojedynczego delegata na ukończone zdarzenie lub co_await go. Jeśli masz obie te elementy, drugi zakończy się niepowodzeniem. Jeden z następujących dwóch rodzajów procedur obsługi uzupełniania jest odpowiedni; nie oba dla tego samego obiektu asynchronicznego.

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

Aby uzyskać więcej informacji na temat procedur obsługi uzupełniania, zobacz Typy delegatów dla akcji asynchronicznych i operacji.

Wystrzel i zapomnij

Czasami masz zadanie, które można wykonać równocześnie z innymi pracami i nie musisz czekać na jego ukończenie (żadna inna praca od niego nie zależy), ani nie musisz otrzymać z niego wartości zwrotnej. W takim przypadku możesz uruchomić zadanie i zapomnieć o nim. Można to zrobić, pisząc coroutine, której typ zwracany jest winrt::fire_and_forget (zamiast jednego z typów operacji asynchronicznych środowiska uruchomieniowego systemu Windows lub współbieżności::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 jest również przydatny jako typ zwracany programu obsługi zdarzeń, gdy trzeba wykonać w nim operacje asynchroniczne. Oto przykład (patrz także silne i słabe referencje w języku 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.
}

Pierwszy argument ( nadawca) jest pozostawiony bez nazwy, ponieważ nigdy go nie używamy. Możemy bez obaw pozostawić go jako odniesienie. Należy jednak zauważyć, że args są przekazywane przez wartość. Zobacz sekcję przekazywania parametrów powyżej.

Oczekiwanie na uchwyt jądra

Język C++/ WinRT udostępnia funkcję winrt::resume_on_signal , której można użyć do wstrzymania do momentu zasygnaliowania zdarzenia jądra. Jesteś odpowiedzialny za zapewnienie, że uchwyt pozostanie ważny, dopóki nie powróci co_await resume_on_signal(h). resume_on_signal sam nie może tego zrobić, ponieważ być może utracono uchwyt jeszcze przed uruchomieniem resume_on_signal, jak w tym pierwszym przykładzie.

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

Przychodząca HANDLE jest prawidłowa tylko do momentu, w którym funkcja zwróci wartość, a ta funkcja (która jest korutyną) zwraca wartość przy pierwszym punkcie zawieszenia (pierwszy co_await w tym przypadku). Podczas oczekiwania na doworkAsynckontrolka została zwrócona do obiektu wywołującego, ramka wywołująca wyszła z zakresu i nie wiesz już, czy uchwyt będzie prawidłowy po wznowieniu coroutine.

Technicznie, nasza kohroutyna otrzymuje parametry według wartości, jak to powinno (zobacz Przekazywanie parametrów powyżej). Ale w tym przypadku musimy pójść o krok dalej, abyśmy kierowali się duchem tych wytycznych (a nie tylko literą). Musimy przekazać silne odwołanie (innymi słowy własność) wraz z uchwytem. Oto jak to zrobić.

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

Przekazywanie winrt::handle według wartości zapewnia semantyka własności, co gwarantuje, że uchwyt jądra pozostaje ważny przez cały okres istnienia kohroutyny.

Oto jak można nazwać to coroutine.

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

Możesz przekazać wartość limitu czasu do resume_on_signal, tak jak w tym przykładzie.

winrt::handle event = ...

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

Łatwe asynchroniczne limity czasu

C++/WinRT jest mocno zaangażowany w koroutyny języka C++. Ich wpływ na pisanie kodu współbieżnego jest transformujący. W tej sekcji omówiono przypadki, w których szczegóły asynchronii nie są ważne, a wszystko, czego potrzebujesz, to natychmiastowy wynik. Z tego powodu implementacja C++/WinRT IAsyncAction interfejsu operacji asynchronicznej środowiska uruchomieniowego Windows ma funkcję get, podobną do tej, którą udostępnia std::future.

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

blokuje funkcję na czas nieokreślony, aż obiekt asynchroniczny zostanie ukończony. Obiekty asynchroniczne wydają się być bardzo krótkotrwałe, więc często jest to wszystko, czego potrzebujesz.

Ale istnieją przypadki, w których nie jest to wystarczające, i musisz porzucić oczekiwanie po upływie pewnego czasu. Pisanie tego kodu zawsze było możliwe dzięki blokom konstrukcyjnym dostarczonym przez środowisko uruchomieniowe systemu Windows. Jednak teraz C++/WinRT czyni udostępnienie funkcji wait_for znacznie łatwiejszym. Jest on również implementowany w usłudze IAsyncAction i ponownie jest podobny do tego, który jest dostarczany przez std::future.

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

Uwaga / Notatka

wait_for używa std::chrono::duration w interfejsie, ale jest ograniczony do zakresu mniejszego niż to, co std::chrono::duration zapewnia (około 49,7 dni).

wait_for w tym kolejnym przykładzie czeka około pięciu sekund, a następnie sprawdza zakończenie. Jeśli porównanie jest korzystne, wiesz, że obiekt asynchroniczny został ukończony pomyślnie i wszystko jest gotowe. Jeśli czekasz na jakiś wynik, możesz po prostu postępować zgodnie z wywołaniem metody GetResults , aby pobrać wynik.

Uwaga / Notatka

"wait_for" i "get" wykluczają się wzajemnie (nie można wywołać ich jednocześnie). Każda z nich liczy się jako kelneri asynchroniczne akcje/operacje środowiska uruchomieniowego systemu Windows obsługują tylko jednego kelnera.

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

Ponieważ obiekt asynchroniczny został ukończony do tego czasu, metoda GetResults zwraca wynik natychmiast, bez dalszego oczekiwania. Jak widać, wait_for zwraca stan obiektu asynchronicznego. Możesz więc użyć go do bardziej szczegółowej kontrolki, takiej jak ta.

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;
}
  • Pamiętaj, że AsyncStatus::Completed oznacza, że obiekt asynchroniczny został ukończony pomyślnie i można wywołać metodę GetResults , aby pobrać dowolny wynik.
  • AsyncStatus::Canceled oznacza, że obiekt asynchroniczny został anulowany. Żądanie anulowania jest zwykle składane przez osobę dzwoniącą, więc rzadko obsługuje się ten stan. Zazwyczaj anulowany obiekt asynchroniczny jest po prostu odrzucany. Jeśli chcesz, możesz wywołać metodę GetResults , aby ponownie wywołać wyjątek anulowania.
  • AsyncStatus::Error oznacza, że obiekt asynchroniczny uległ awarii w jakiś sposób. Jeśli chcesz, możesz wywołać metodę GetResults , aby ponownie wywołać wyjątek.
  • AsyncStatus::Started oznacza, że obiekt asynchroniczny jest nadal uruchomiony. Wzorzec asynchroniczny środowiska uruchomieniowego systemu Windows nie zezwala na wiele jednoczesnych oczekiwań ani oczekujących. Oznacza to, że nie można wywołać wait_for w pętli. Jeśli limit czasu oczekiwania został skutecznie przekroczony, masz do wyboru kilka opcji. Możesz porzucić obiekt lub sondować jego stan przed wywołaniem metody GetResults w celu pobrania dowolnego wyniku. Ale najlepiej jest odrzucić obiekt w tym momencie.

Alternatywnym schematem jest sprawdzenie tylko Rozpoczętei pozwolić funkcji GetResults zająć się innymi przypadkami.

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

Zwracanie tablicy asynchronicznie

Poniżej przedstawiono przykład MIDL 3.0, który generuje błąd MIDL2025: [msg] błąd składni [context]: oczekiwano > lub, w pobliżu "[",.

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

Powodem jest to, że nieprawidłowe jest użycie tablicy jako argumentu typu parametru dla interfejsu sparametryzowanego. Dlatego potrzebujemy mniej oczywistego sposobu osiągnięcia celu asynchronicznego przekazywania tablicy z powrotem z metody klasy środowiska uruchomieniowego.

Możesz zwrócić tablicę zapakowaną w obiekt PropertyValue. Kod wywołujący następnie rozpakowuje to. Oto przykład kodu, który można wypróbować, dodając klasę środowiska uruchomieniowego SampleComponent do projektu składnika środowiska uruchomieniowego systemu Windows (C++/WinRT) , a następnie konsumując go z projektu (na przykład) Core App (C++/WinRT).

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

Ważne interfejsy API