Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Este tópico descreve cenários avançados com concorrência e assíncronia em C++/WinRT.
Para uma introdução a este tema, leia primeiro Concorrência e operações assíncronas.
Delegar trabalho ao pool de threads do Windows
Uma corrotina é uma função como qualquer outra, no sentido em que um chamador é bloqueado até que uma função lhe retorne a execução. E, a primeira oportunidade para uma corrotina regressar é a primeira co_await, co_return, ou co_yield.
Portanto, antes de fazeres trabalho limitado de computação numa corrotina, precisas de devolver a execução ao chamador (ou seja, introduzir um ponto de suspensão) para que o chamador não fique bloqueado. Se ainda não estiveres a fazê-lo ao co_awaitexecutar alguma outra operação, então podes co_await chamar a função winrt::resume_background. Isso devolve o controlo ao chamador e depois retoma de imediato a execução numa thread da thread pool.
O pool de threads usado na implementação é o pool de threads do Windows de baixo nível, por isso é otimizadamente eficiente.
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;
}
Programação tendo em conta a afinidade das threads
Este cenário expande o anterior. Descarregas algum trabalho para o pool de threads, mas depois queres mostrar o progresso na interface de utilizador (UI).
IAsyncAction DoWorkAsync(TextBlock textblock)
{
co_await winrt::resume_background();
// Do compute-bound work here.
textblock.Text(L"Done!"); // Error: TextBlock has thread affinity.
}
O código acima lança uma exceção winrt::hresult_wrong_thread , porque um TextBlock tem de ser atualizado a partir do thread que o criou, que é o thread UI. Uma solução é capturar o contexto da thread em que a nossa corrotina foi originalmente chamada. Para isso, instancia um objeto winrt::apartment_context , faz trabalho em segundo plano e depois co_await o apartment_context para voltar ao contexto de chamada.
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.
}
Desde que a corrotina acima seja chamada a partir do thread da interface que criou o TextBlock, esta técnica funciona. Haverá muitos casos na tua aplicação em que tens a certeza disso.
Para uma solução mais geral para atualizar a interface, que cobre casos em que não tens a certeza sobre o thread que chama, podes co_await usar a função winrt::resume_foreground para mudar para um thread específico em primeiro plano. No exemplo de código abaixo, especificamos o thread em primeiro plano passando pela fila do despachante associada ao TextBlock (acedendo à sua propriedade DispatcherQueue ). A implementação do winrt::resume_foreground chama o DispatcherQueue.TryEnqueue nesse objeto da fila do dispatcher para executar o trabalho que vem depois na corrotina.
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.
}
A função winrt::resume_foreground recebe um parâmetro de prioridade opcional. Se estiver a usar esse parâmetro, então o padrão mostrado acima é apropriado. Se não, então pode optar por simplificar co_await winrt::resume_foreground(someDispatcherObject); apenas para co_await someDispatcherObject;.
Contextos de execução, retoma e troca numa corrotina
De um modo geral, após um ponto de suspensão numa corrotina, o thread original de execução pode desaparecer e a retomada pode ocorrer em qualquer thread (ou seja, qualquer thread pode chamar o método Completed para a operação assíncrona).
Mas se escolher co_await qualquer um dos quatro tipos de operações assíncronas do Windows Runtime (IAsyncXxx), então o C++/WinRT captura o contexto de chamada no ponto em que co_await. E garante que ainda estás nesse contexto quando a continuação recomeça. O C++/WinRT faz isto verificando se já está no contexto de chamada e, se não estiver, mudando para esse contexto. Se estavas numa thread de apartment single-threaded (STA) antes de co_await, então estarás na mesma depois; se estavas numa thread de apartment multi-threaded (MTA) antes de co_await, então estarás numa depois.
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.
}
A razão pela qual se pode confiar neste comportamento é porque o C++/WinRT fornece código para adaptar esses tipos de operações assíncronas do Windows Runtime ao suporte da linguagem de corrotinas C++ (estes pedaços de código são chamados adaptadores de espera). Os restantes tipos aguardáveis em C++/WinRT são simplesmente encapsulamentos e/ou auxiliares do agrupamento de threads; pelo que são concluídos no agrupamento de threads.
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;
}
Se tiveres co_await outro tipo — mesmo dentro de uma implementação de corrotina C++/WinRT — então outra biblioteca fornece os adaptadores, e terás de perceber o que esses adaptadores fazem em termos de retomada e contextos.
Para manter as trocas de contexto ao mínimo, pode usar algumas das técnicas que já vimos neste tópico. Vamos ver algumas ilustrações de como fazer isso. Neste próximo exemplo de pseudo-código, mostramos o contorno de um gestor de eventos que chama uma API do Windows Runtime para carregar uma imagem, desce para um thread em segundo plano para processar essa imagem, e depois regressa ao thread da interface para mostrar a imagem na interface.
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.
}
Neste cenário, há alguma ineficiência na chamada para StorageFile::OpenAsync. É necessária uma mudança de contexto para uma thread em segundo plano (para que o processador possa devolver o controlo da execução ao chamador), após a retoma, após a qual o C++/WinRT restaura o contexto da thread da IU. Mas, neste caso, não é necessário estar no tópico da interface até estarmos prestes a atualizá-la. Quanto mais APIs do Windows Runtime invocarmos antes da chamada a winrt::resume_background, mais mudanças de contexto desnecessárias de um lado para o outro teremos de suportar. A solução é não chamar nenhuma API do Windows Runtime antes disso. Move-os todos depois do 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.
}
Se quiseres fazer algo mais avançado, podes escrever os teus próprios adaptadores de await. Por exemplo, se quiser que um co_await seja retomado na mesma thread na qual a ação assíncrona é concluída (ou seja, não há troca de contexto), pode começar por escrever adaptadores de await semelhantes aos apresentados abaixo.
Note
O exemplo do código abaixo é fornecido apenas para fins educativos; Serve para começar a perceber como funcionam os adaptadores Wait. Se quiser usar esta técnica na sua própria base de código, recomendamos que desenvolva e teste as suas próprias estruturas adaptadoras de await. Por exemplo, podes escrever complete_on_any, complete_on_current e complete_on(dispatcher). Considera também torná-los modelos que recebam o tipo IAsyncXxx como parâmetro de modelo.
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;
};
Para perceberes como usar os adaptadores no_switch await, primeiro precisas de saber que, quando o compilador C++ encontra uma co_await expressão, procura funções chamadas await_ready, await_suspend e await_resume. A biblioteca C++/WinRT fornece essas funções para que se obtenha um comportamento razoável por defeito, como o seguinte.
IAsyncAction async{ ProcessFeedAsync() };
co_await async;
Para usar os adaptadores no_switch await, basta mudar o tipo dessa co_await expressão de IAsyncXxx para no_switch, assim.
IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);
Depois, em vez de procurar as três funções await_xxx que correspondem ao IAsyncXxx, o compilador C++ procura funções que correspondam a no_switch.
Uma análise mais aprofundada de winrt::resume_foreground
A partir da versão C++/WinRT 2.0, a função winrt::resume_foreground fica suspensa mesmo que seja chamada a partir da thread do dispatcher (em versões anteriores, podia introduzir deadlocks em alguns cenários porque só ficava suspensa se ainda não estivesse na thread do dispatcher).
O comportamento atual significa que pode contar com o desenrolamento da pilha e com a nova colocação em fila; e isso é importante para a estabilidade do sistema, especialmente em código de sistema de baixo nível. A última listagem de código na secção Programação com afinidade de threads em mente, acima, ilustra como efetuar um cálculo complexo numa thread em segundo plano e, em seguida, mudar para a thread da IU adequada para atualizar a interface de utilizador (IU).
Eis o aspeto interno de winrt::resume_foreground.
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{ ... };
};
Este comportamento atual, em comparação com o anterior, é análogo à diferença entre PostMessage e SendMessage no desenvolvimento de aplicações Win32. O PostMessage coloca o trabalho na fila e depois desenrola a pilha sem esperar que o trabalho termine. O desenrolamento da pilha pode ser essencial.
A função winrt::resume_foreground suportava originalmente o CoreDispatcher (ligado a um CoreWindow), que foi introduzido antes de Windows 10. Nas aplicações WinUI 3 e SDK de Aplicações Windows, utilize antes a DispatcherQueue. Pode criar uma DispatcherQueue para os seus próprios fins. Considere esta aplicação simples de consola.
using namespace Windows::System;
winrt::fire_and_forget RunAsync(DispatcherQueue queue);
int main()
{
auto controller{ DispatcherQueueController::CreateOnDedicatedThread() };
RunAsync(controller.DispatcherQueue());
getchar();
}
O exemplo acima cria uma fila (contida dentro de um controlador) numa thread privada e depois passa o controlador para a corrotina. A corutina pode usar a fila para aguardar (suspender e retomar) na thread privada. Outro uso comum do DispatcherQueue é criar uma fila na thread atual da interface para uma aplicação tradicional de desktop ou 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 };
}
Isto ilustra como pode chamar e incorporar funções Win32 nos seus projetos C++/WinRT, simplesmente chamando a função CreateDispatcherQueueController ao estilo Win32 para criar o controlador, e depois transferir a propriedade do controlador de fila resultante para o chamador como um objeto WinRT. É também precisamente assim que pode suportar uma fila eficiente e fluida na sua aplicação de desktop Win32 ao estilo 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);
}
}
Acima, a função principal simples começa por criar uma janela. Pode imaginar que isto regista uma classe de janela e chama o CreateWindow para criar a janela do ambiente de trabalho de topo. A função CreateDispatcherQueueController é então chamada para criar o controlador da fila antes de chamar uma corrotina com a fila de despacho associada a este controlador. É então introduzida uma bomba de mensagens tradicional, onde a retomada da corrotina ocorre naturalmente neste fio. Depois disso, pode regressar ao elegante mundo das corrotinas para o seu fluxo de trabalho assíncrono ou baseado em mensagens dentro da sua aplicação.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
... // Begin on the calling thread...
co_await winrt::resume_foreground(queue);
... // ...resume on the dispatcher thread.
}
A chamada a winrt::resume_foreground irá sempre ser colocada em fila e, em seguida, desenrolar a stack. Também pode, opcionalmente, definir a prioridade de retomada.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
...
}
Ou, usando a ordem padrão da fila.
...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
co_await queue;
...
}
Note
Conforme mostrado acima, certifique-se de incluir o cabeçalho de projeção para o espaço de nomes do tipo para o qual está a fazer co_await-ing. Por exemplo, Windows::System::DispatcherQueue ou Microsoft::UI::Dispatching::DispatcherQueue.
Ou, neste caso, detetar o encerramento da fila e lidar com isso com elegância.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
if (co_await queue)
{
... // Resume on dispatcher thread.
}
else
{
... // Still on calling thread.
}
}
A expressão co_await devolve true, indicando que a execução será retomada na thread do dispatcher. Ou seja, essa fila foi bem-sucedida. Por outro lado, regressa false para indicar que a execução permanece na thread que chama porque o controlador da fila está a desligar-se e já não está a servir pedidos de fila.
Assim, tens muito poder ao teu alcance quando combinas C++/WinRT com corrotinas; e especialmente ao fazer algum desenvolvimento de aplicações desktop ao estilo Petzold à moda antiga.
Cancelar uma operação assíncrona e as funções de retorno de cancelamento
As funcionalidades do Windows Runtime para programação assíncrona permitem-lhe cancelar uma ação ou operação assíncrona em voo. Aqui está um exemplo que chama StorageFolder::GetFilesAsync para recuperar uma coleção potencialmente grande de ficheiros, e armazena o objeto de operação assíncrono resultante num membro de dados. O utilizador tem a opção de cancelar a operação.
// 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;
};
...
No que diz respeito à implementação do cancelamento, vamos começar com um exemplo simples.
// 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();
}
Se executares o exemplo acima, vais ver o ImplicitCancelationAsync imprimir uma mensagem por segundo durante três segundos, após o que termina automaticamente devido ao cancelamento. Isto funciona porque, ao encontrar a expressão co_await, a corrotina verifica se foi cancelada. Se fizer, então entra em curto-circuito; E se não estiver, então suspende-se normalmente.
O cancelamento pode, naturalmente, acontecer enquanto a corrotina está suspensa. Só quando a corrotina for retomada, ou atingir outro co_await, irá verificar se houve cancelamento. O problema reside numa latência potencialmente de granularidade demasiado grossa na resposta a pedidos de cancelamento.
Assim, outra opção é verificar explicitamente se houve cancelamento dentro da sua corrotina. Atualize o exemplo acima com o código na listagem abaixo. Neste novo exemplo, ExplicitCancelationAsync recupera o objeto devolvido pela função winrt::get_cancellation_token e utiliza-o para verificar periodicamente se a corrotina foi cancelada. Enquanto não for cancelada, a corrotina executa-se indefinidamente em ciclo; assim que for cancelada, o ciclo e a função terminam normalmente. O resultado é o mesmo do exemplo anterior, mas aqui a saída acontece explicitamente e sob controlo.
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();
}
...
Ao aguardar winrt::get_cancellation_token, obtém um token de cancelamento que tem conhecimento da IAsyncAction que a corrotina está a produzir por si. Pode usar o operador de chamada de função nesse token para consultar o estado de cancelamento — essencialmente para sondar o cancelamento. Se estiveres a realizar alguma operação limitada por computação, ou a iterar por uma grande coleção, então esta é uma técnica razoável.
Registe um callback de cancelamento
O cancelamento do Windows Runtime não se propaga automaticamente para outros objetos assíncronos. Mas — introduzida na versão 10.0.17763.0 (Windows 10, versão 1809) do SDK Windows — pode registar um callback de cancelamento. Este é um gancho preemptivo pelo qual o cancelamento pode ser propagado, tornando possível integrar com bibliotecas de concorrência existentes.
Neste próximo exemplo de código, o NestedCoroutineAsync faz o trabalho, mas não tem nenhuma lógica especial de cancelamento. CancelationPropagatorAsync é essencialmente um invólucro da corrotina aninhada; o invólucro propaga o cancelamento antecipadamente.
// 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 regista uma função lambda para o seu próprio callback de cancelamento, e depois esta aguarda (suspende) até que o trabalho aninhado seja concluído. Quando ou se o CancellationPropagatorAsync for cancelado, propaga o cancelamento para a corrotina aninhada. Não há necessidade de pedir cancelamento; nem o cancelamento é bloqueado indefinidamente. Este mecanismo é suficientemente flexível para o utilizar para interoperar com uma corrotina ou biblioteca de concorrência que não conhece C++/WinRT.
Comunicação dos progressos realizados
Se a sua cortina devolver IAsyncActionWithProgress ou IAsyncOperationWithProgress, então pode recuperar o objeto devolvido pela função winrt::get_progress_token e usá-lo para reportar progresso a um gestor de progresso. Aqui está um exemplo de código.
// 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();
}
Para comunicar o progresso, invoque o token de progresso, passando o valor do progresso como argumento. Para estabelecer um resultado provisório, utilize o método set_result() no token de progresso.
Note
Reportar resultados provisórios requer C++/WinRT versão 2.0.210309.3 ou posterior.
O exemplo acima escolhe definir um resultado provisório para cada relatório de progresso. Pode optar por reportar resultados provisórios a qualquer momento, se é que o fizer. Não precisa de ser acompanhado de um relatório de progresso.
Note
Não é correto implementar mais do que um handler de completação para uma ação ou operação assíncrona. Pode ter um único delegado para o evento concluído, ou co_await pode fazê-lo. Se tiveres ambos, o segundo falha. Qualquer um dos seguintes dois tipos de manipuladores de conclusão é apropriado; Não ambos para o mesmo objeto assíncrono.
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 };
Para mais informações sobre manipuladores de conclusão, veja Tipos delegate para ações e operações assíncronas.
Fogo e esquece
Por vezes, tens uma tarefa que pode ser feita em simultâneo com outro trabalho, e não precisas de esperar que essa tarefa seja concluída (nenhum outro trabalho depende dela), nem precisas que ela devolva um valor. Nesse caso, podes iniciar a tarefa e esquecê-la. Pode fazê-lo escrevendo uma corrotina cujo tipo de retorno é winrt::fire_and_forget (em vez de um dos tipos de operações assíncronas do Windows Runtime, ou 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 também é útil como tipo de retorno do seu gestor de eventos quando precisa de realizar operações assíncronas nele. Aqui está um exemplo (veja também Referências fortes e fracas em 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.
}
O primeiro argumento (o emissor) é deixado sem nome, porque nunca o usamos. Por essa razão, podemos deixá-lo como referência. Mas repare que o args é passado por valor. Veja a secção sobre passagem de parâmetros acima.
À espera de um kernel handle
O C++/WinRT fornece uma função winrt::resume_on_signal , que pode usar para suspender até que um evento do kernel seja sinalizado. És responsável por garantir que o identificador se mantém válido até que o teu co_await resume_on_signal(h) regresse. O próprio resume_on_signal não pode fazer isso por si só, porque pode ter perdido a referência antes sequer de o resume_on_signal começar, como neste primeiro exemplo.
IAsyncAction Async(HANDLE event)
{
co_await DoWorkAsync();
co_await resume_on_signal(event); // The incoming handle is not valid here.
}
O HANDLE recebido é válido apenas até a função regressar, e esta função (que é uma corrotina) retorna no primeiro ponto de suspensão (o primeiro co_await neste caso). Enquanto aguarda o DoWorkAsync, o controlo voltou ao chamador, o frame de chamada saiu do âmbito e já não sabe se o handle será válido quando a sua corrotina retomar.
Tecnicamente, a nossa corrotina recebe os parâmetros por valor, como é suposto (ver Passagem de parâmetros acima). Mas, neste caso, precisamos de ir mais longe para seguirmos o espírito dessa orientação (e não apenas a letra). Precisamos de passar uma referência forte (ou seja, propriedade) juntamente com o alçadão. Veja como.
IAsyncAction Async(winrt::handle event)
{
co_await DoWorkAsync();
co_await resume_on_signal(event); // The incoming handle *is* valid here.
}
Passar um winrt::handle por valor confere semântica de posse, o que garante que o identificador do kernel permanece válido durante toda a vida útil da corrotina.
É assim que se pode chamar isso de corrotina.
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.
}
Pode passar um valor de timeout a resume_on_signal, como neste exemplo.
winrt::handle event = ...
if (co_await winrt::resume_on_signal(event.get(), std::literals::2s))
{
puts("signaled");
}
else
{
puts("timed out");
}
Timeouts assíncronos facilitados
O C++/WinRT assenta fortemente em corrotinas de C++. O seu efeito na escrita de código de concorrência é transformador. Esta secção discute casos em que os detalhes da assíncronia não são importantes, e tudo o que se quer é o resultado ali mesmo. Por essa razão, a implementação da interface de operação assíncrona IAsyncAction Windows Runtime em C++/WinRT tem uma função get, semelhante à fornecida pelo std::future.
using namespace winrt::Windows::Foundation;
int main()
{
IAsyncAction async = ...
async.get();
puts("Done!");
}
A função get bloqueia indefinidamente, enquanto o objeto assíncrono se completa. Os objetos assíncronos tendem a ter vida muito curta, por isso isto é muitas vezes tudo o que precisas.
Mas há casos em que isso não é suficiente, e é preciso abandonar a espera depois de algum tempo. Escrever esse código sempre foi possível, graças aos blocos de construção fornecidos pelo Windows Runtime. Mas agora o C++/WinRT facilita muito ao fornecer a função wait_for . Também está implementado no IAsyncAction, e novamente é semelhante ao fornecido pelo std::future.
using namespace std::chrono_literals;
int main()
{
IAsyncAction async = ...
if (async.wait_for(5s) == AsyncStatus::Completed)
{
puts("done");
}
}
Note
wait_for usa std::chrono::duration na sua interface, mas está limitada a um intervalo inferior ao disponibilizado por std::chrono::duration (cerca de 49,7 dias).
O wait_for neste próximo exemplo espera cerca de cinco segundos e depois verifica a conclusão. Se a comparação for positiva, então sabe que o objeto assíncrono foi concluído com êxito e está tudo concluído. Se estiver à espera de algum resultado, pode simplesmente ligar ao método GetResults para recuperar o resultado.
Note
wait_for e get são mutuamente exclusivos (não podes invocar ambos os métodos). Cada um conta como uma entidade à espera, e as ações/operações assíncronas do Windows Runtime suportam apenas uma única entidade à espera.
int main()
{
IAsyncOperation<int> async = ...
if (async.wait_for(5s) == AsyncStatus::Completed)
{
printf("result %d\n", async.GetResults());
}
}
Como o objeto assíncrono já foi concluído nessa altura, o método GetResults devolve o resultado imediatamente, sem mais espera. Como podes ver, wait_for devolve o estado do objeto assíncrono. Assim, podes usá-lo para um controlo mais detalhado, como este.
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;
}
- Lembre-se que AsyncStatus::Completed significa que o objeto assíncrono foi concluído com sucesso, e pode chamar o método GetResults para recuperar qualquer resultado.
- AsyncStatus::Canceled significa que o objeto assíncrono foi cancelado. O cancelamento é normalmente solicitado pelo interlocutor, por isso seria raro lidar com este estado. Normalmente, um objeto assíncrono cancelado é simplesmente descartado. Pode chamar o método GetResults para relançar a exceção de cancelamento, se quiser.
- AsyncStatus::Error significa que o objeto assíncrono falhou de alguma forma. Pode chamar o método GetResults para relançar a exceção, se quiser.
- AsyncStatus::Started significa que o objeto assíncrono ainda está a correr. O padrão assíncrono do Windows Runtime não permite múltiplas esperas, nem empregados de mesa. Isso significa que não podes invocar wait_for dentro de um ciclo. Se a espera já esgotou, ficam com algumas opções. Pode abandonar o objeto, ou pode consultar o seu estado antes de chamar o método GetResults para recuperar qualquer resultado. Mas é melhor descartar o objeto neste momento.
Um padrão alternativo é verificar apenas o Iniciado e deixar o GetResults tratar dos outros casos.
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();
}
Devolver um array de forma assíncrona
Abaixo está um exemplo do MIDL 3.0 que produz erro MIDL2025: [msg]erro de sintaxe [contexto]: esperando > ou, perto de "[".
Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();
A razão é que é inválido usar um array como argumento de tipo de parâmetro para uma interface parametrizada. Por isso, precisamos de uma forma menos óbvia de atingir o objetivo de devolver assincronamente um array a partir de um método de uma classe de runtime.
Pode retornar a matriz encapsulada num objeto PropertyValue. O código de chamada então desencaixa-o. Aqui está um exemplo de código, que pode experimentar adicionando a classe de runtime SampleComponent a um projeto Windows Runtime Component (C++/WinRT) e, em seguida, utilizá-la a partir de, por exemplo, um projeto Blank App, Packaged (WinUI 3 em Ambiente de Trabalho).
// 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.
...
APIs importantes
- Interface IAsyncAction
- Interface IAsyncActionWithProgress<TProgress>
- interface IAsyncOperation<TResult>
- interface IAsyncOperationWithProgress<TResult, TProgress>
- SyndicationClient::RetrieveFeedAsync método
- winrt::fire_and_forget
- winrt::get_cancellation_token
- winrt::get_progress_token
- winrt::resume_foreground
Tópicos relacionados
Windows developer