Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Toto téma popisuje pokročilé scénáře souběžnosti a asynchronní synchronizace v C++/WinRT.
Úvod do tohoto předmětu si nejprve přečtěte Souběžnost a asynchronní operace.
Přesunutí práce do fondu vláken systému Windows
Korutina je funkce jako každá jiná v tom, že volající je blokován, dokud mu funkce nepředá řízení zpět. A první možností, kdy se korutina může vrátit, je první co_await, co_return nebo co_yield.
Než tedy v korutině začnete provádět výpočetně náročnou práci, musíte předat řízení zpět volajícímu (jinými slovy, vložit bod suspendace), aby se volající neblokoval. Pokud to ještě neděláte pomocí jiné co_await-ing operace, můžete co_await funkci winrt::resume_background. Tím se řízení vrátí volajícímu a poté ihned pokračuje provádění na vlákně z fondu vláken.
Fond vláken, který se používá v implementaci, je fond vláken nízké úrovně Windows, takže je optimálně efektivní.
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;
}
Programování s ohledem na afinitu vláken
Tento scénář se rozšiřuje o předchozí scénář. Část práce přesunete do fondu vláken, ale pak chcete v uživatelském rozhraní (UI) zobrazit průběh.
IAsyncAction DoWorkAsync(TextBlock textblock)
{
co_await winrt::resume_background();
// Do compute-bound work here.
textblock.Text(L"Done!"); // Error: TextBlock has thread affinity.
}
Výše uvedený kód vyvolá výjimku winrt::hresult_wrong_thread , protože TextBlock musí být aktualizován z vlákna, které ho vytvořilo, což je vlákno uživatelského rozhraní. Jedním z řešení je zachytit kontext vlákna, v jehož rámci byla naše korutina původně volána. Uděláte to tak, že vytvoříte instanci objektu winrt::apartment_context, provedete operace na pozadí a potom pomocí co_awaitapartment_context přepnete zpět do volajícího kontextu.
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.
}
Pokud je výše uvedená korutina volána z vlákna UI, které vytvořilo TextBlock, pak tato technika funguje. V aplikaci bude mnoho případů, kdy jste si jistí.
Pro obecnější řešení aktualizace uživatelského rozhraní, které pokrývá případy, kdy si nejste jisti, které vlákno volá, můžete použít funkci co_awaitwinrt::resume_foreground k přepnutí na konkrétní vlákno popředí. V příkladu kódu níže určíme vlákno na popředí předáním fronty dispečeru přidružené k TextBlock (přístupem k jeho vlastnosti DispatcherQueue). Implementace winrt::resume_foreground volá DispatcherQueue.TryEnqueue u tohoto objektu fronty dispečera, aby se provedl kód, který po něm v korutině následuje.
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.
}
Funkce winrt::resume_foreground přebírá volitelný parametr priority. Pokud používáte tento parametr, je vhodný vzor uvedený výše. Pokud ne, můžete se rozhodnout, že zjednodušíte co_await winrt::resume_foreground(someDispatcherObject); jen co_await someDispatcherObject;.
Kontexty spuštění, obnovení a přepínání v korutině
Obecně řečeno, po bodu pozastavení v korutině již původní vlákno vykonávání nemusí existovat a vykonávání může být obnoveno v libovolném vlákně (jinými slovy, metodu Completed pro asynchronní operaci může zavolat libovolné vlákno).
Pokud ale co_await některý ze čtyř typů asynchronních operací prostředí Windows Runtime (IAsyncXxx), pak C++/WinRT zachytí volající kontext v okamžiku, kdy co_await. A zajišťuje, že při opětovném navázání pokračování zůstanete ve stejném kontextu. C++/WinRT to dělá tak, že zkontrolujete, jestli už používáte kontext volání, a pokud ne, přepnete na něj. Pokud jste před co_await běželi v jednovláknovém vlákně jednotky (STA), budete v něm běžet i potom; pokud jste před co_await běželi ve vícevláknovém vlákně jednotky (MTA), budete potom běžet v některém z nich.
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.
}
Důvod, proč se na toto chování můžete spolehnout, spočívá v tom, že C++/WinRT poskytuje kód, který tyto typy asynchronních operací prostředí Windows Runtime přizpůsobuje jazykové podpoře korutin v C++ (tyto části kódu se nazývají adaptéry čekání). Zbývající typy, které lze v C++/WinRT awaitovat, jsou jednoduše obaly nad fondem vláken a/nebo pomocné typy; a proto se dokončují ve fondu vláken.
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;
}
Pokud v rámci implementace korutiny C++/WinRT použijete co_await jiný typ, pak další knihovna poskytuje adaptéry a budete muset pochopit, co tyto adaptéry dělají z hlediska obnovení a kontextu.
Pokud chcete zachovat kontextové přepínače na minimum, můžete použít některé techniky, které jsme už viděli v tomto tématu. Podívejme se na některé ilustrace, jak to udělat. V tomto dalším příkladu pseudokódu ukážeme nástin obslužné rutiny události, která volá rozhraní prostředí Windows Runtime API pro načtení obrázku, poté přepne zpracování tohoto obrázku na vlákno na pozadí a nakonec se vrátí do vlákna UI, aby obrázek zobrazila v uživatelském rozhraní.
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.
}
V tomto scénáři je volání StorageFile::OpenAsync trochu neefektivní. Je nutné provést přepnutí kontextu do vlákna na pozadí (aby obslužná rutina mohla vrátit řízení volajícímu), po jehož obnovení C++/WinRT obnoví kontext vlákna UI. V tomto případě ale není nutné být ve vlákně uživatelského rozhraní, dokud se chystáme aktualizovat uživatelské rozhraní. Čím více API prostředí Windows Runtime voláme před voláním winrt::resume_background, tím více zbytečného přepínání kontextu tam a zpět tím způsobíme. Řešením je do té doby nevolat žádná rozhraní API prostředí Windows Runtime. Přesuňte je všechny za 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.
}
Pokud chcete provést něco pokročilejšího, mohli byste si napsat vlastní adaptéry pro await. Pokud například chcete, aby se co_await obnovilo ve stejném vlákně, na kterém se asynchronní akce dokončí (takže nedojde k přepnutí kontextu), můžete začít napsáním adaptérů pro await podobných těm, které jsou uvedeny níže.
Note
Níže uvedený příklad kódu je poskytován pouze pro vzdělávací účely; Je potřeba začít pochopit, jak fungují adaptéry await. Pokud chcete použít tuto techniku ve vlastním základu kódu, doporučujeme vyvíjet a testovat vlastní struktury adaptérů await. Mohli byste například napsat complete_on_any, complete_on_current a complete_on(dispatcher). Zvažte také vytvoření šablon, které jako parametr šablony přebírají typ IAsyncXxx .
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;
};
Abyste pochopili, jak používat adaptéry await no_switch, musíte nejprve vědět, že když kompilátor jazyka C++ narazí na výraz co_await, hledá funkce s názvy await_ready, await_suspend a await_resume. Knihovna C++/WinRT poskytuje tyto funkce, abyste ve výchozím nastavení získali přiměřené chování, jako je tento.
IAsyncAction async{ ProcessFeedAsync() };
co_await async;
Pokud chcete použít adaptéry no_switch await, změňte typ tohoto výrazu co_await z IAsyncXxx na no_switch, například takto.
IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);
Místo hledání tří await_xxx funkcí, které odpovídají IAsyncXxx, hledá kompilátor jazyka C++ funkce, které odpovídají no_switch.
Podrobnější informace o winrtu::resume_foreground
Od verze C++/WinRT 2.0 se funkce winrt::resume_foreground pozastaví, i když je volána z vlákna dispečera (v předchozích verzích může v některých scénářích zavádět zablokování, protože se pozastaví, pouze pokud ještě není ve vlákně dispečera).
Současné chování znamená, že se můžete spolehnout na to, že dochází k odvíjení zásobníku a opětovnému zařazení do fronty; a to je důležité pro stabilitu systému, zejména v nízkoúrovňovém systémovém kódu. Poslední kód uvedený v části Programování s spřažením vláken výše ukazuje provádění některých složitých výpočtů na vlákně na pozadí a následným přepnutím na příslušné vlákno uživatelského rozhraní, aby bylo možné aktualizovat uživatelské rozhraní.
Takto vypadá winrt::resume_foreground interně.
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{ ... };
};
Toto současné a předchozí chování je podobné rozdílu mezi PostMessage a SendMessage ve vývoji aplikací Win32. PostMessage zařadí práci do fronty a pak odpojí zásobník bez čekání na dokončení práce. Rozvinutí zásobníku může být nezbytné.
Funkce winrt::resume_foreground původně podporovala CoreDispatcher (svázaný s CoreWindow), která byla zavedena před Windows 10. V aplikacích WinUI 3 a Windows App SDK místo toho použijte DispatcherQueue. DispatcherQueue můžete vytvořit pro vlastní účely. Zvažte tuto jednoduchou konzolovou aplikaci.
using namespace Windows::System;
winrt::fire_and_forget RunAsync(DispatcherQueue queue);
int main()
{
auto controller{ DispatcherQueueController::CreateOnDedicatedThread() };
RunAsync(controller.DispatcherQueue());
getchar();
}
Výše uvedený příklad vytvoří frontu (obsaženou v kontroleru) v privátním vlákně a pak předá kontroler korutině. Korutin může pomocí fronty čekat (pozastavit a obnovit) v privátním vlákně. Dalším běžným použitím DispatcherQueue je vytvoření fronty v aktuálním vlákně uživatelského rozhraní pro tradiční desktopovou aplikaci nebo aplikaci 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 };
}
To ukazuje, jak můžete volat funkce Win32 a začlenit je do svých projektů C++/WinRT, a to jednoduše voláním funkce ve stylu Win32 CreateDispatcherQueueController za účelem vytvoření kontroleru a následným předáním vlastnictví výsledného kontroleru fronty volajícímu jako objektu WinRT. Právě takto můžete ve vaší stávající desktopové aplikaci Win32 ve stylu Petzolda zajistit efektivní a bezproblémovou práci s frontami.
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);
}
}
Výše jednoduchá funkce main začíná vytvořením okna. Můžete si představit, že tím zaregistrujete třídu okna a zavoláte CreateWindow , aby se vytvořilo okno plochy nejvyšší úrovně. Funkce CreateDispatcherQueueController se poté zavolá, aby se vytvořil kontroler fronty před voláním nějaké korutiny s frontou dispečeru, kterou tento kontroler vlastní. Poté se vstoupí do tradičního čerpadla zpráv, kde k obnovení korutiny přirozeně dochází v tomto vlákně. Jakmile to uděláte, můžete se vrátit do elegantního světa korutin pro asynchronní pracovní postup nebo postup založený na zprávách ve své aplikaci.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
... // Begin on the calling thread...
co_await winrt::resume_foreground(queue);
... // ...resume on the dispatcher thread.
}
Volání winrt::resume_foreground bude vždy zařazeno do fronty a potom rozbalí zásobník. Volitelně můžete také nastavit prioritu obnovení.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
...
}
Nebo použijte výchozí pořadí front.
...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
co_await queue;
...
}
Note
Jak je uvedeno výše, nezapomeňte zahrnout hlavičkový soubor projekce pro obor názvů typu, který pomocí co_await používáte. Například Windows::System::DispatcherQueue nebo Microsoft::UI::Dispatching::DispatcherQueue.
Nebo v tomto případě detekovat vypnutí fronty a elegantně ho zpracovat.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
if (co_await queue)
{
... // Resume on dispatcher thread.
}
else
{
... // Still on calling thread.
}
}
Výraz co_await vrátí hodnotu true, což znamená, že pokračování proběhne ve vlákně dispečera. Jinými slovy, zařazení do fronty bylo úspěšné. Naopak vrátí false, aby signalizoval, že vykonávání zůstává ve volajícím vlákně, protože řadič fronty se vypíná a již neobsluhuje požadavky fronty.
Takže máte k dispozici značné možnosti, když zkombinujete C++/WinRT s koroutinami, a zvlášť při klasickém vývoji desktopových aplikací v Petzoldově stylu.
Zrušení asynchronní operace a zpětná volání při zrušení
Funkce prostředí Windows Runtime pro asynchronní programování umožňují zrušit asynchronní akci nebo operaci v letu. Tady je příklad, který volá StorageFolder::GetFilesAsync k načtení potenciálně velké kolekce souborů a uloží výsledný objekt asynchronní operace v datovém členu. Uživatel má možnost operaci zrušit.
// 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;
};
...
Pokud jde o implementaci rušení, začněme jednoduchým příkladem.
// 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();
}
Pokud spustíte výše uvedený příklad, uvidíte, že ImplicitCancelationAsync bude po dobu tří sekund vypisovat jednu zprávu za sekundu, načež se automaticky ukončí v důsledku zrušení. To funguje, protože při výskytu výrazu co_await korutin kontroluje, jestli byl zrušen. Pokud ano, okamžitě se ukončí; a pokud ne, normálně se pozastaví.
Ke zrušení může samozřejmě dojít i během pozastavení korutiny. Teprve když korutina pokračuje nebo narazí na další co_await, zkontroluje, zda byla zrušena. Problém spočívá v potenciálně příliš hrubě odstupňované latenci při reakci na zrušení.
Další možností je explicitně kontrolovat zrušení přímo ve své korutině. Aktualizujte výše uvedený příklad kódem v následujícím výpisu. V tomto novém příkladu explicitCancelationAsync načte objekt vrácený funkcí winrt::get_cancellation_token a používá ho k pravidelné kontrole, zda byl korutin zrušen. Dokud není korutina zrušena, opakuje se ve smyčce donekonečna; jakmile je zrušena, smyčka i funkce se normálně ukončí. Výsledek je stejný jako v předchozím příkladu, ale tady dochází k ukončení explicitně a pod kontrolou.
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();
}
...
Čekání na winrt::get_cancellation_token získá token zrušení, který má povědomí o IAsyncAction, kterou korutina vytváří vaším jménem. Operátor volání funkce na daném tokenu můžete použít k dotazování stavu zrušení – v podstatě dotazování na zrušení. Pokud provádíte nějakou výpočetní operaci nebo iterujete velkou kolekcí, je to rozumná technika.
Zaregistrovat zpětné volání pro zrušení
Zrušení v prostředí prostředí Windows Runtime se automaticky nepřenáší na jiné asynchronní objekty. Ale – zaveden ve verzi 10.0.17763.0 (Windows 10, verze 1809) sady Windows SDK – můžete zaregistrovat zpětné volání zrušení. Jedná se o preventivní mechanismus, pomocí kterého lze propagovat zrušení a který umožňuje integraci se stávajícími knihovnami pro souběžné zpracování.
V tomto dalším příkladu kódu funguje NestedCoroutineAsync , ale nemá v něm žádnou speciální logiku zrušení. CancelationPropagatorAsync je v podstatě obal nad vnořenou korutinou; tento obal preventivně propaguje zrušení.
// 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 registruje funkci lambda jako vlastní zpětné volání při zrušení a poté čeká (pozastaví se), dokud se nedokončí vnořená operace. Pokud nebo pokud CancellationPropagatorAsync je zrušen, rozšíří zrušení do vnořené koroutiny. Není třeba průběžně kontrolovat, zda bylo požadováno zrušení; ani není možné zrušení blokovat neomezeně dlouho. Tento mechanismus je dostatečně flexibilní, abyste jej mohli použít k interoperabilitě s knihovnou pro korutiny nebo souběžnost, která o C++/WinRT nic neví.
Vykazování průběhu
Pokud vaše korutina vrací buď IAsyncActionWithProgress, nebo IAsyncOperationWithProgress, pak můžete získat objekt vrácený funkcí winrt::get_progress_token a použít ho k hlášení o průběhu obslužné rutině průběhu. Tady je příklad kódu.
// 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();
}
Chcete-li hlásit průběh, zavolejte token `progress` a jako argument předejte hodnotu průběhu. Chcete-li nastavit průběžný výsledek, použijte metodu set_result() u tokenu průběhu.
Note
Vykazování předběžných výsledků vyžaduje C++/WinRT verze 2.0.210309.3 nebo novější.
Výše uvedený příklad se rozhodne nastavit předběžný výsledek pro každou zprávu o průběhu. Průběžné výsledky můžete oznamovat kdykoli, nebo také vůbec. Není nutné ji spojit se zprávou o průběhu.
Note
Není správné implementovat více než jednu obslužnou rutinu dokončení pro asynchronní akci nebo operaci. Můžete mít buď jednoho delegáta pro dokončenou událost, nebo můžete co_await . Pokud máte obě, druhý selže. Vhodný je kterýkoli z následujících dvou typů obslužných rutin pro dokončení; nikoli však oba pro tentýž asynchronní 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 };
Další informace o obslužných rutinách pro dokončení najdete v tématu Typy delegátů pro asynchronní akce a operace.
Oheň a zapomenutí
Někdy máte úkol, který se dá provést souběžně s jinou prací a nemusíte čekat na dokončení úkolu (žádná jiná práce na něm nezávisí), ani ji nepotřebujete k vrácení hodnoty. V takovém případě můžete úlohu spustit a zapomenout na ni. Můžete to udělat tak, že napíšete korutinu, jejíž návratový typ je winrt::fire_and_forget (namísto jednoho z typů asynchronních operací prostředí prostředí Windows Runtime nebo 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 je užitečný také jako návratový typ obslužné funkce události, pokud v ní potřebujete provádět asynchronní operace. Tady je příklad (viz také silné a slabé odkazy v jazyce 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.
}
První argument ( odesílatel) zůstane nepojmenovaný, protože ho nikdy nepoužíváme. Z tohoto důvodu je bezpečné ji nechat jako referenci. Všimněte si ale, že args je předáván hodnotou. Viz část s předáváním parametrů výše.
Čeká se na identifikátor jádra
C++/WinRT poskytuje funkci winrt::resume_on_signal , kterou můžete použít k pozastavení, dokud nebude událost jádra signalována. Zodpovídáte za to, že popisovač zůstane platný, dokud se co_await resume_on_signal(h) nevrátí.
resume_on_signal to samo o sobě nedokáže zajistit, protože jste možná přišli o handle ještě předtím, než se resume_on_signal spustí, jak ukazuje tento první příklad.
IAsyncAction Async(HANDLE event)
{
co_await DoWorkAsync();
co_await resume_on_signal(event); // The incoming handle is not valid here.
}
Vstupní HANDLE je platný pouze do návratu funkce a tato funkce (což je korutina) se vrací při prvním bodu přerušení (v tomto případě při prvním co_await). Zatímco čekáte na DoWorkAsync, ovládací prvek se vrátil volajícímu, volající rámec zmizel z rozsahu a už nevíte, jestli bude popisovač platný při obnovení korutiny.
Technicky vzato naše korutina přijímá své parametry hodnotou, jak má (viz výše uvedené předávání parametrů). Ale v tomto případě musíme jít o krok dál, abychom se řídili duchem těchto pokynů (a ne jen jejich literou). Musíme spolu s handlem předat silnou referenci (tj. vlastnictví). Tady je postup.
IAsyncAction Async(winrt::handle event)
{
co_await DoWorkAsync();
co_await resume_on_signal(event); // The incoming handle *is* valid here.
}
Předání winrt::handle hodnotou poskytuje sémantiku vlastnictví, která zajišťuje, že popisovač jádra zůstane platný po celou dobu životnosti korutiny.
Takto můžete zavolat tuto korutinu.
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.
}
Hodnotu časového limitu můžete předat resume_on_signal, jak je znázorněno v tomto příkladu.
winrt::handle event = ...
if (co_await winrt::resume_on_signal(event.get(), std::literals::2s))
{
puts("signaled");
}
else
{
puts("timed out");
}
Asynchronní časové limity se zjednodušily.
C++/WinRT je silně investován do koroutin jazyka C++. Jejich vliv na psaní souběžného kódu je zásadní. Tato část pojednává o případech, kdy podrobnosti asynchronního zpracování nejsou důležité a potřebujete pouze okamžitý výsledek. Z tohoto důvodu má implementace asynchronního operačního rozhraní prostředí Windows Runtime IAsyncAction v C++/WinRT funkci get, podobnou té, kterou poskytuje std::future.
using namespace winrt::Windows::Foundation;
int main()
{
IAsyncAction async = ...
async.get();
puts("Done!");
}
Funkce get blokuje na neurčito, dokud se asynchronní objekt nedokončí. Asynchronní objekty mají tendenci být velmi krátkodobé, takže to je často vše, co potřebujete.
Ale existují případy, kdy to nestačí, a potřebujete opustit čekání po uplynutí určité doby. Psaní tohoto kódu bylo vždy možné díky stavebním blokům poskytovaným prostředí Windows Runtime. Teď ale C++/WinRT usnadňuje poskytování funkce wait_for . Je také implementována v IAsyncAction a znovu je podobná tomu, co poskytuje std::future.
using namespace std::chrono_literals;
int main()
{
IAsyncAction async = ...
if (async.wait_for(5s) == AsyncStatus::Completed)
{
puts("done");
}
}
Note
wait_for používá std::chrono::d uration v rozhraní, ale je omezena na určitý rozsah menší než std::chrono::d uration poskytuje (přibližně 49,7 dnů).
Wait_for v tomto dalším příkladu počká přibližně pět sekund a pak zkontroluje dokončení. Pokud je porovnání příznivé, pak víte, že asynchronní objekt byl úspěšně dokončen a jste hotovi. Pokud čekáte na nějaký výsledek, můžete ho jednoduše sledovat voláním metody GetResults , která načte výsledek.
Note
wait_for a get se navzájem vylučují (nemůžete volat obě). Každý se počítá jako číšník a prostředí Windows Runtime asynchronní akce nebo operace podporují pouze jeden číšník.
int main()
{
IAsyncOperation<int> async = ...
if (async.wait_for(5s) == AsyncStatus::Completed)
{
printf("result %d\n", async.GetResults());
}
}
Protože asynchronní objekt byl dokončen do té doby, GetResults metoda vrátí výsledek okamžitě, bez dalšího čekání. Jak vidíte, wait_for vrátí stav asynchronního objektu. Můžete ho tedy použít k jemněji odstupňovanému ovládání, jako je tento.
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;
}
- Mějte na paměti, že AsyncStatus::Completed znamená, že asynchronní objekt byl úspěšně dokončen, a můžete zavolat metodu GetResults a získat případný výsledek.
- AsyncStatus::Canceled znamená, že asynchronní objekt byl zrušen. Volající obvykle požaduje zrušení, takže zpracování tohoto stavu může být vzácné. Zrušený asynchronní objekt se obvykle jednoduše zahodí. Pokud si přejete, můžete zavolat metodu GetResults a znovu vyvolat výjimku zrušení.
- AsyncStatus::Error znamená, že asynchronní objekt nějakým způsobem selhal. Pokud si to přejete, můžete zavolat metodu GetResults a znovu vyvolat výjimku.
- AsyncStatus::Started znamená, že asynchronní objekt je stále spuštěný. Asynchronní vzor prostředí Windows Runtime neumožňuje více čekání ani číšníků. To znamená, že nemůžete volat wait_for v cyklu. Pokud během čekání skutečně vypršel časový limit, zbývá vám několik možností. Objekt můžete zahodit nebo můžete před voláním metody GetResults zjišťovat jeho stav, abyste načetli libovolný výsledek. V tuto chvíli je ale nejlepší objekt zahodit.
Alternativní postup spočívá v tom, že se kontroluje pouze Started a ostatní případy se přenechají funkci GetResults.
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();
}
Asynchronní vrácení pole
Níže je příklad MIDL 3.0, který generuje chybu MIDL2025: [msg]chyba syntaxe [context]: očekává se > nebo v blízkosti "[".
Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();
Důvodem je, že použití pole jako argumentu typu parametru pro parametrizované rozhraní je neplatné. Proto potřebujeme méně zřejmý způsob, jak dosáhnout cíle asynchronního předávání pole zpět z metody třídy runtime.
Pole můžete vrátit zabalené do objektu PropertyValue. Volající kód jej pak rozbalí. Tady je příklad kódu, který si můžete vyzkoušet tak, že do projektu prostředí Windows Runtime Component (C++/WinRT) přidáte běhovou třídu SampleComponent a potom ji použijete například z projektu Blank App, Packaged (WinUI 3 in Desktop).
// 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.
...
Důležitá rozhraní API
- IAsyncAction – rozhraní
- Rozhraní IAsyncActionWithProgress<TProgress>
- IAsyncOperation<TResult> – rozhraní
- IAsyncOperationWithProgress<TResult, TProgress> – rozhraní
- Metoda SyndicationClient::RetrieveFeedAsync
- winrt::fire_and_forget
- winrt::get_cancellation_token
- winrt::get_progress_token
- winrt::resume_foreground
Příbuzná témata
Windows developer