Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
In dit onderwerp worden geavanceerde scenario's beschreven met gelijktijdigheid en asynchroniteit in verband met C++/WinRT.
Lees voor een inleiding tot dit onderwerp eerst Gelijktijdigheid en asynchrone bewerkingen.
Het overdragen van werk naar de Windows-draadpool
Een coroutine is een functie zoals elke andere, waarbij de aanroeper wordt geblokkeerd totdat de functie de uitvoering eraan teruggeeft. En de eerste kans voor een coroutine om terug te keren is de eerste co_await
, co_return
of co_yield
.
Dus voordat u rekengebonden werk uitvoert in een coroutine, moet u de uitvoering teruggeven aan de aanroeper (met andere woorden, een suspendiepunt introduceren) zodat de aanroeper niet wordt geblokkeerd. Als u dat niet al doet door een andere bewerking uit te voeren met co_await
, kunt u de functie co_await
de winrt::resume_background gebruiken. Dit retourneert de controle naar de beller en hervat vervolgens onmiddellijk de uitvoering op een thread uit de thread pool.
De threadpool die in de implementatie wordt gebruikt, is de Windows-threadgroep op laag niveau, zodat deze optimaal efficiënt is.
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;
}
Programmeren met threadaffiniteit in het achterhoofd
In dit scenario wordt het vorige scenario uitgebreid. U kunt wat werk offloaden naar de threadpool, maar u wilt de voortgang weergeven in de gebruikersinterface (UI).
IAsyncAction DoWorkAsync(TextBlock textblock)
{
co_await winrt::resume_background();
// Do compute-bound work here.
textblock.Text(L"Done!"); // Error: TextBlock has thread affinity.
}
Met de bovenstaande code wordt een winrt::hresult_wrong_thread uitzondering gegenereerd, omdat een TextBlock- moet worden bijgewerkt vanuit de thread die deze gecreëerd heeft, wat namelijk de UI-thread betreft. Een oplossing is het vastleggen van de threadcontext waarin onze coroutine oorspronkelijk werd aangeroepen. U doet dit door een winrt::apartment_context-object te instantiëren, achtergrondwerk uit te voeren en co_await
vervolgens de apartment_context om terug te gaan naar de aanroepende context.
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.
}
Zolang de bovenstaande coroutine wordt aangeroepen vanuit de UI-thread die de TextBlockheeft gemaakt, werkt deze techniek. Er zijn veel gevallen in uw app waar u zeker van bent.
Voor een algemenere oplossing voor het bijwerken van de gebruikersinterface, waarin gevallen worden behandeld waarin u niet zeker bent over de aanroepende thread, kunt u de co_await
-functie om over te schakelen naar een specifieke voorgrondthread. In het onderstaande codevoorbeeld specificeren we de voorgrondthread door het dispatcherobject door te geven dat gekoppeld is aan de TextBlock (door toegang te krijgen tot de eigenschap van de Dispatcher). De implementatie van winrt::resume_foreground roept CoreDispatcher.RunAsync op dat dispatcherobject aan om het werk uit te voeren dat erna in de coroutine komt.
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.
}
De functie winrt::resume_foreground heeft een optionele prioriteitsparameter. Als u deze parameter gebruikt, is het bovenstaande patroon geschikt. Zo niet, dan kunt u ervoor kiezen om co_await winrt::resume_foreground(someDispatcherObject);
te vereenvoudigen in slechts co_await someDispatcherObject;
.
Uitvoeringscontexten, hervatten en overschakelen in een coroutine
In grote lijnen kan na een ophangingspunt in een coroutine de oorspronkelijke uitvoerende thread verdwijnen en kan de uitvoering hervat worden op een willekeurige thread (met andere woorden, elke thread kan de methode Voltooide voor de asynchrone bewerking aanroepen).
Maar als u een van de vier asynchrone bewerkingstypen van Windows Runtime co_await
(IAsyncXxx), legt C++/WinRT de aanroepende context vast op het punt dat u co_await
. En het zorgt ervoor dat u zich nog steeds in die context bevindt wanneer de voortzetting wordt hervat. C++/WinRT doet dit door te controleren of u zich al in de aanroepende context bevindt en, als dat niet het geval is, naar deze context over te schakelen. Als u een sta-thread (single threaded apartment) had voordat co_await
, dan bent u daarna op dezelfde; als je op een MTA-thread (multi-threaded apartment) was voordat co_await
, dan ben je er daarna op een.
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.
}
De reden waarom u op dit gedrag kunt vertrouwen, is omdat C++/WinRT code biedt om deze asynchrone bewerkingstypen van Windows Runtime aan te passen aan de C++-ondersteuning voor coroutinetaal (deze stukjes code worden wachtadapters genoemd). De resterende wachtbare typen in C++/WinRT zijn gewoon threadpool-wrappers en/of hulpprogramma's; ze worden voltooid in de threadpool.
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;
}
Als u een ander type operatie co_await
uitvoert, zelfs binnen een C++/WinRT-coroutine-implementatie, worden de adapters door een andere bibliotheek geleverd en moet u begrijpen wat deze adapters doen met betrekking tot hervatting en contexten.
Om contextwisselingen tot een minimum te beperken, kun je enkele van de technieken gebruiken die we al in dit onderwerp hebben gezien. Laten we eens wat illustraties bekijken om dat te doen. In dit volgende pseudocodevoorbeeld laten we het overzicht zien van een gebeurtenis-handler die een Windows Runtime-API aanroept om een afbeelding te laden, neer te zetten op een achtergrondthread om die afbeelding te verwerken en vervolgens terugkeert naar de UI-thread om de afbeelding in de gebruikersinterface weer te geven.
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.
}
Voor dit scenario is er een beetje inefficiëntie rond de aanroep naar StorageFile::OpenAsync. Er is een noodzakelijke contextswitch naar een achtergrondthread (zodat de handler de uitvoering naar de aanroeper kan retourneren), na hervatting waarna C++/WinRT de context van de UI-thread herstelt. Maar in dit geval is het niet nodig om in de ui-thread te zijn totdat de gebruikersinterface wordt bijgewerkt. Hoe meer Windows Runtime-API's we aanroepen voordat we onze aanroep naar winrt::resume_backgrounddoen, hoe meer onnodige contextwisselingen we ondergaan. De oplossing is om vóór die tijd geen Windows Runtime-API's aan te roepen
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.
}
Als u iets geavanceerder wilt doen, kunt u uw eigen await-adapters schrijven. Als u bijvoorbeeld wilt dat een co_await
hervat op dezelfde thread waarop de asynchrone actie is voltooid (dus er vindt geen contextswitch plaats), kunt u beginnen met het schrijven van await-adapters die vergelijkbaar zijn met de hieronder weergegeven adapters.
Opmerking
Het onderstaande codevoorbeeld is alleen bedoeld voor educatieve doeleinden; het is om u te helpen begrijpen hoe await-adapters werken. Als u deze techniek in uw eigen codebasis wilt gebruiken, raden we u aan uw eigen await adapter-struct(en) te ontwikkelen en te testen. U kunt bijvoorbeeld complete_on_any, complete_on_currenten complete_on(dispatcher)schrijven. U kunt er ook voor kiezen om ze sjablonen te maken die het type IAsyncXxx als sjabloonparameter gebruiken.
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;
};
Als u wilt weten hoe u de no_switch await-adapters gebruikt, moet u eerst weten dat wanneer de C++-compiler een co_await
expressie tegenkomt, het zoekt naar functies met de naam await_ready, await_suspenden await_resume. De C++/WinRT-bibliotheek biedt deze functies, zodat u standaard redelijk gedrag krijgt, zoals dit.
IAsyncAction async{ ProcessFeedAsync() };
co_await async;
Als u de no_switch wachtadapters gebruikt, wijzigt u het type van die co_await
-expressie van IAsyncXxx- in no_switch, zoals hieronder getoond.
IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);
In plaats van te zoeken naar de drie await_xxx functies die overeenkomen met IAsyncXxx-, zoekt de C++-compiler naar functies die overeenkomen met no_switch.
Een diepgaande verkenning van winrt::resume_foreground
Vanaf C++/WinRT 2.0, wordt de functie winrt::resume_foreground opgeschort, zelfs als deze wordt aangeroepen vanuit de dispatcher-thread (in eerdere versies kon het deadlocks veroorzaken in sommige scenario's, omdat deze alleen werd opgeschort als deze nog niet op de dispatcher-thread stond).
Het huidige gedrag betekent dat u kunt vertrouwen op het ontwinden van de stapel en het opnieuw in de wachtrij plaatsen; en dat is belangrijk voor de stabiliteit van het systeem, vooral in code op laag niveau. De laatste codevermelding in de sectie Programmeren met threadaffiniteit in gedachten, hierboven, illustreert het uitvoeren van een complexe berekening op een achtergrondthread en vervolgens overschakelen naar de juiste UI-thread om de gebruikersinterface (UI) bij te werken.
Zo ziet winrt::resume_foreground er intern uit.
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{ ... };
};
Dit huidige, versus vorige gedrag is vergelijkbaar met het verschil tussen PostMessage en SendMessage- in win32-toepassingsontwikkeling. PostMessage plaatst het werk in de wachtrij en wikkelt dan de stack af zonder te wachten tot het werk voltooid is. Het terugdraaien van de stack kan essentieel zijn.
De functie winrt::resume_foreground ondersteunt in eerste instantie ook alleen de CoreDispatcher- (gekoppeld aan een CoreWindow-), die vóór Windows 10 werd geïntroduceerd. We hebben sindsdien een flexibelere en efficiëntere dispatcher geïntroduceerd: de DispatcherQueue. U kunt voor uw eigen doeleinden een DispatcherQueue- maken. Houd rekening met deze eenvoudige consoletoepassing.
using namespace Windows::System;
winrt::fire_and_forget RunAsync(DispatcherQueue queue);
int main()
{
auto controller{ DispatcherQueueController::CreateOnDedicatedThread() };
RunAsync(controller.DispatcherQueue());
getchar();
}
In het bovenstaande voorbeeld wordt een wachtrij (opgenomen in een controller) gemaakt op een privéthread en wordt de controller vervolgens doorgegeven aan de coroutine. De coroutine kan de wachtrij gebruiken om te wachten (onderbreken en hervatten) op de privéthread. Een ander veelvoorkomend gebruik van DispatcherQueue is het maken van een wachtrij op de huidige UI-thread voor een traditionele bureaublad- of Win32-app.
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 };
}
Dit laat zien hoe u Win32-functies kunt aanroepen en opnemen in uw C++/WinRT-projecten door de Win32-stijl CreateDispatcherQueueController functie aan te roepen om de controller te maken en vervolgens het eigendom van de resulterende wachtrijcontroller over te dragen aan de beller als een WinRT-object. Dit is ook precies hoe u efficiënte en naadloze wachtrijen op uw bestaande Petzold-stijl Win32-bureaubladtoepassing kunt ondersteunen.
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);
}
}
Hierboven begint de eenvoudige hoofdfunctie door een venster te maken. U kunt zich voorstellen dat dit een vensterklasse registreert en CreateWindow- aanroept om het bureaubladvenster op het hoogste niveau te maken. De functie CreateDispatcherQueueController wordt vervolgens aangeroepen om de wachtrijcontroller te maken voordat een bepaalde coroutine wordt aangeroepen met de dispatcher-queue die eigendom is van deze controller. Een traditionele berichtpomp wordt vervolgens ingevoerd, waarin de hervatting van de coroutine gewoonlijk op deze draad gebeurt. Als u dat hebt gedaan, kunt u terugkeren naar de elegante wereld van coroutines voor uw asynchrone of berichtengebaseerde werkstroom in uw toepassing.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
... // Begin on the calling thread...
co_await winrt::resume_foreground(queue);
... // ...resume on the dispatcher thread.
}
De oproep naar winrt::resume_foreground zal altijd wachtrijen vervolgens de stapel ontspannen. U kunt eventueel ook de hervattingsprioriteit instellen.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
...
}
Of gebruik de standaardvolgorde voor wachtrijen.
...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
co_await queue;
...
}
Opmerking
Zoals hierboven wordt weergegeven, moet u de projectieheader opnemen in de naamruimte van het type dat u co_await
maakt. Bijvoorbeeld Windows::UI::Core::CoreDispatcher, Windows::System::DispatcherQueueof Microsoft::UI::Dispatching::DispatcherQueue.
Of, in dit geval het detecteren van het afsluiten van de wachtrij en het op een soepele manier afhandelen.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
if (co_await queue)
{
... // Resume on dispatcher thread.
}
else
{
... // Still on calling thread.
}
}
De co_await
-expressie retourneert true
, waarmee wordt aangegeven dat hervatting plaatsvindt op de dispatcherthread. Met andere woorden, die wachtrij is geslaagd. Omgekeerd retourneert het false
om aan te geven dat de uitvoering bij de aanroepende thread blijft, omdat de controller van de wachtrij wordt afgesloten en geen wachtrijaanvragen meer verwerkt.
U hebt dus veel kracht binnen handbereik wanneer u C++/WinRT combineert met coroutines; en vooral bij het ontwikkelen van een ouderwetse Petzold-stijl desktopapplicatie.
Een asynchrone bewerking annuleren en callbacks annuleren
Met de functies van Windows Runtime voor asynchrone programmering kunt u een asynchrone actie of bewerking tijdens de vlucht annuleren. Hier volgt een voorbeeld dat StorageFolder::GetFilesAsync aanroept om een mogelijk grote verzameling bestanden op te halen en het resulterende asynchrone bewerkingsobject opslaat in een gegevenslid. De gebruiker heeft de mogelijkheid om de bewerking te annuleren.
// 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;
};
...
Laten we beginnen met een eenvoudig voorbeeld voor de implementatiezijde van annulering.
// 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();
}
Als u het bovenstaande voorbeeld uitvoert, ziet u dat ImplicieteCancelationAsync gedurende drie seconden één bericht per seconde afdrukt, waarna deze automatisch wordt beëindigd als gevolg van het annuleren. Dit werkt omdat bij het tegenkomen van een co_await
expressie een coroutine controleert of deze is geannuleerd. Als dat zo is, veroorzaakt het een kortsluiting; en als dat niet zo is, wordt het normaal onderbroken.
Annulering kan natuurlijk gebeuren terwijl de coroutine gepauzeerd is. Alleen wanneer de coroutine hervat wordt of een andere co_await
bereikt, controleert het op annulering. Het probleem is dat er mogelijk sprake is van te grofkorrelige latentie bij het annuleren.
Een andere optie is om expliciet te peilen naar annulering vanuit uw coroutine. Werk het bovenstaande voorbeeld bij met de code in de onderstaande vermelding. In dit nieuwe voorbeeld haalt ExplicitCancelationAsync het object op dat wordt geretourneerd door de functie winrt::get_cancellation_token en wordt dit gebruikt om periodiek te controleren of de coroutine is geannuleerd. Zolang het niet wordt geannuleerd, herhaalt de coroutine zich oneindig; zodra het is geannuleerd, stoppen de lus en de functie normaal. Het resultaat is hetzelfde als in het vorige voorbeeld, maar hier gebeurt het afsluiten expliciet en onder controle.
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();
}
...
Wachten op winrt::get_cancellation_token haalt een annuleringstoken op met kennis van de IAsyncAction die de coroutine namens u produceert. U kunt de operator voor functieoproep op dat token gebruiken om een query uit te voeren op de annuleringsstatus, waarbij in wezen periodiek wordt gecontroleerd op annulering. Als u een berekeningsgebonden bewerking uitvoert of een grote verzameling herhaalt, is dit een redelijke techniek.
Een annuleringsaanroep registreren
De annulering van Windows Runtime stroomt niet automatisch naar andere asynchrone objecten. Maar, geïntroduceerd in versie 10.0.17763.0 (Windows 10, versie 1809) van de Windows SDK, kunt u een annuleringsaanroep registreren. Dit is een preventieve hook waarmee annulering kan worden doorgegeven en maakt het mogelijk om te integreren met bestaande gelijktijdigheidsbibliotheken.
In dit volgende codevoorbeeld voert NestedCoroutineAsync- het werk uit, maar heeft er geen speciale annuleringslogica in. CancelationPropagatorAsync is in feite een wrapper op de geneste coroutine; de wrapper stuurt annulering vooraf door.
// 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 registreert een lambda-functie voor zijn eigen annuleringsaanroep en wacht vervolgens (wordt opgeschort) totdat het geneste werk is voltooid. Wanneer of als CancellationPropagatorAsync wordt geannuleerd, wordt de annulering doorgegeven aan de geneste coroutine. Het is niet nodig om te peilen op annulering; of wordt annulering voor onbepaalde tijd geblokkeerd. Dit mechanisme is flexibel genoeg om het te gebruiken om te interopen met een coroutine- of gelijktijdigheidsbibliotheek die niets van C++/WinRT kent.
Voortgang rapporteren
Als uw coroutine ofwel IAsyncActionWithProgressretourneert, of IAsyncOperationWithProgress, kunt u het object ophalen dat wordt geretourneerd door de winrt::get_progress_token-functie en deze gebruiken om de voortgang weer te rapporteren aan een voortgangshandler. Hier volgt een codevoorbeeld.
// 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();
}
Als u de voortgang wilt rapporteren, roept u het voortgangstoken aan met de voortgangswaarde als argument. Als u een voorlopig resultaat wilt instellen, gebruikt u de set_result()
methode voor het voortgangstoken.
Opmerking
Voor het rapporteren van voorlopige resultaten is C++/WinRT versie 2.0.210309.3 of hoger vereist.
In het bovenstaande voorbeeld wordt ervoor gekozen om een voorlopig resultaat in te stellen voor elk voortgangsrapport. U kunt ervoor kiezen om op elk gewenst moment voorlopige resultaten te rapporteren. Het rapport hoeft niet te worden gekoppeld aan een voortgangsrapport.
Opmerking
Het is niet juist om meer dan één voltooiingshandler te implementeren voor een asynchrone actie of bewerking. U kunt of een enkele gedelegeerde hebben voor het evenement dat is voltooid, of u kunt co_await
. Als u beide hebt, mislukt de tweede. Een van de volgende twee soorten voltooiingshandlers is geschikt; niet beide voor hetzelfde asynchrone object.
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 };
Zie Typen gemachtigden voor asynchrone acties en bewerkingenvoor meer informatie over voltooiingshandlers.
Schiet en vergeet
Soms heb je een taak die gelijktijdig met ander werk kan worden uitgevoerd, en waarbij je niet hoeft te wachten tot die taak is voltooid (geen ander werk is ervan afhankelijk), noch heb je het nodig om een waarde terug te geven. In dat geval kunt u de taak afvuren en vergeten. U kunt dit doen door een coroutine te schrijven met als retourtype winrt::fire_and_forget (in plaats van een van de asynchrone bewerkingstypes van Windows Runtime of 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 is ook nuttig als retourtype van uw event handler wanneer u hierin asynchrone bewerkingen moet uitvoeren. Hier volgt een voorbeeld (zie ook Sterke en zwakke verwijzingen 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.
}
Het eerste argument (de afzender) wordt niet-benoemd, omdat we het nooit gebruiken. Daarom is het veilig om het als referentie te houden. Maar bedenk dat argumenten als waarde worden doorgegeven. Zie de sectie parameter-passing hierboven.
Wachten op een kernel-handle
C++/WinRT biedt een winrt::resume_on_signal-functie, die u kunt gebruiken om te onderbreken totdat een kernelgebeurtenis wordt gesignaleerd. U bent verantwoordelijk voor het waarborgen dat de handle geldig blijft totdat uw co_await resume_on_signal(h)
terugkeert.
resume_on_signal zelf kan dat niet voor je doen, omdat je de referentie mogelijk hebt verloren, zelfs voordat de resume_on_signal begint, zoals in het eerste voorbeeld.
IAsyncAction Async(HANDLE event)
{
co_await DoWorkAsync();
co_await resume_on_signal(event); // The incoming handle is not valid here.
}
De inkomende HANDLE is alleen geldig totdat de functie terugkeert, en deze functie, die een coroutine is, keert terug bij het eerste pauzepunt (de eerste co_await
in dit geval). Terwijl u wacht op DoWorkAsync-, is het besturingselement teruggegaan naar de beller, is het aanroepende frame buiten het bereik gegaan en weet u niet meer of de ingang geldig is wanneer uw coroutine wordt hervat.
Technisch gezien ontvangt onze coroutine de parameters bij waarde, zoals het hoort (zie Parameter-passing hierboven). Maar in dit geval moeten we een stap verder gaan, zodat we de geest volgen van die richtlijnen (in plaats van alleen de letter). We moeten een sterke verwijzing (met andere woorden, eigendom) doorgeven, samen met de handle. Dit doet u als volgt.
IAsyncAction Async(winrt::handle event)
{
co_await DoWorkAsync();
co_await resume_on_signal(event); // The incoming handle *is* valid here.
}
Het doorgeven van een winrt::handle als waarde biedt eigendomssymantiek, waardoor de kernelhandle geldig blijft gedurende de levensduur van de coroutine.
Hier is hoe u die coroutine kunt aanroepen.
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.
}
U kunt een time-outwaarde doorgeven aan resume_on_signal, zoals in dit voorbeeld.
winrt::handle event = ...
if (co_await winrt::resume_on_signal(event.get(), std::literals::2s))
{
puts("signaled");
}
else
{
puts("timed out");
}
Asynchrone time-outs zijn eenvoudig gemaakt
C++/WinRT legt sterk de nadruk op C++ coroutines. Hun effect op het schrijven van gelijktijdigheidscode is transformatief. In deze sectie worden gevallen besproken waarin details van asynchroniciteit er niet toe doen, en u alleen het resultaat daar en direct wilt. Daarom heeft de implementatie van de Windows Runtime asynchrone bewerkingsinterface IAsyncAction in C++/WinRT een -functie, vergelijkbaar met die van std::future.
using namespace winrt::Windows::Foundation;
int main()
{
IAsyncAction async = ...
async.get();
puts("Done!");
}
De functie blokkeert voor onbepaalde tijd, terwijl het asynchrone object wordt voltooid. Asynchrone objecten hebben meestal een korte levensduur, dus dit is vaak alles wat u nodig hebt.
Maar er zijn gevallen waarin dat niet voldoende is en u de wachttijd moet verlaten nadat enige tijd is verstreken. Het schrijven van die code is altijd mogelijk, dankzij de bouwstenen van De Windows Runtime. Maar nu maakt C++/WinRT het veel eenvoudiger door de wait_for functie op te geven. Het is ook geïmplementeerd op IAsyncAction, en nogmaals is het vergelijkbaar met die van std::future.
using namespace std::chrono_literals;
int main()
{
IAsyncAction async = ...
if (async.wait_for(5s) == AsyncStatus::Completed)
{
puts("done");
}
}
Opmerking
wait_for gebruikt std::chrono::duration op de interface, maar het is beperkt tot een bereik dat kleiner is dan wat std::chrono::duration biedt (ongeveer 49,7 dagen).
Het wait_for in dit volgende voorbeeld wacht ongeveer vijf seconden en controleert vervolgens de voltooiing. Als de vergelijking gunstig is, weet u dat het asynchrone object is voltooid en u klaar bent. Als u wacht op een resultaat, kunt u dat gewoon volgen met een aanroep naar de GetResults methode om het resultaat op te halen.
Opmerking
wait_for en get sluiten elkaar uit (u kunt ze niet allebei tegelijkertijd aanroepen). Ze tellen elk als een wachtendeen Windows Runtime asynchrone acties en bewerkingen ondersteunen slechts één wachtende.
int main()
{
IAsyncOperation<int> async = ...
if (async.wait_for(5s) == AsyncStatus::Completed)
{
printf("result %d\n", async.GetResults());
}
}
Omdat het asynchrone object vervolgens is voltooid, retourneert de methode GetResults het resultaat onmiddellijk, zonder verdere wachttijd. Zoals u ziet, retourneert wait_for de status van het asynchrone object. U kunt het dus gebruiken voor meer verfijnde controle, zoals deze.
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;
}
- Houd er rekening mee dat AsyncStatus::Completed betekent dat het asynchrone object is voltooid en u de methode GetResults kunt aanroepen om een resultaat op te halen.
- AsyncStatus::Canceled betekent dat het asynchrone object is geannuleerd. Een annulering wordt doorgaans aangevraagd door de beller, dus het is zeldzaam om deze status te moeten verwerken. Normaal gesproken wordt een geannuleerd asynchroon object gewoon verwijderd. U kunt de methode GetResults aanroepen om de annuleringsuitzondering opnieuw te werpen als u dat wilt.
- AsyncStatus::Error betekent dat het asynchrone object op een of andere manier is mislukt. U kunt de methode GetResults aanroepen om de uitzondering opnieuw te werpen als u dat wilt.
- AsyncStatus::Started betekent dat het asynchrone object nog steeds wordt uitgevoerd. Het Windows Runtime asynchrone patroon staat geen meerdere wachttaken of wachtenden toe. Dat betekent dat u wait_for niet in een lus kunt aanroepen. Als de wachttijd effectief verstrijkt, hebt u nog een paar keuzes. U kunt het object verlaten of de status ervan peilen voordat u de methode GetResults aanroept om een resultaat op te halen. Maar het is het beste om het object op dit moment te negeren.
Een alternatief patroon is om alleen te controleren op Starteden getResults te laten omgaan met de andere gevallen.
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();
}
Een matrix asynchroon retourneren
Hieronder ziet u een voorbeeld van MIDL 3.0 dat error MIDL2025 produceert: [msg]syntaxisfout [context]: verwacht > of, in de buurt van "[".
Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();
De reden hiervoor is dat het ongeldig is om een matrix als parametertypeargument te gebruiken voor een geparameteriseerde interface. We hebben dus een minder voor de hand liggende manier nodig om asynchroon een matrix terug te geven vanuit een runtimeklassemethode.
U kunt de matrix in een PropertyValue-object retourneren. De aanroepende code pakt het vervolgens uit. Hier volgt een codevoorbeeld, dat u kunt uitproberen door de SampleComponent runtime-klasse toe te voegen aan een Windows Runtime Component (C++/WinRT) project en dat vervolgens te gebruiken vanuit (bijvoorbeeld) een Core App (C++/WinRT) project.
// 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.
...
Belangrijke API's
- IAsyncAction-interface
- IAsyncActionWithProgress<TProgress-interface>
- IAsyncOperation<TResult>-interface
- IAsyncOperationWithProgress<TResult, TProgress-interface>
- SyndicationClient::RetrieveFeedAsync-methode
- winrt::fire_and_forget
- winrt::get_cancellation_token
- winrt::get_progress_token
- winrt::resume_foreground