Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Este tópico descreve cenários avançados com simultaneidade e assíncrona no C++/WinRT.
Para obter uma introdução a este assunto, primeiro leia a simultaneidade e as operações assíncronas.
Transferindo tarefas para o pool de threads do Windows
Uma corrotina é uma função como qualquer outra, em que um chamador é bloqueado até que uma função retorna a execução para ele. E a primeira oportunidade para uma corrotina retornar é o primeiro co_await, co_return ou co_yield.
Assim, antes de executar tarefas associadas à computação em uma corrotina, é necessário retornar a execução para o autor da chamada para que ele não seja bloqueado (ou seja, introduzir um ponto de suspensão). Se ainda não estiver fazendo isso por co_await de alguma outra operação, será possível fazer o co_await da função winrt::resume_background. Isso retorna o controle para o autor da chamada e continua a execução imediatamente em um thread do pool de threads.
O pool de threads que está sendo usado na implementação é o pool de threads de Windows de baixo nível, portanto, ele é idealmente 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 pensada para afinidade de threads
Esse cenário se expande no anterior. Você delega parte do trabalho para o pool de threads, mas depois quer exibir o progresso na interface do usuário (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 gera uma exceção winrt::hresult_wrong_thread , pois um TextBlock deve ser atualizado do thread que o criou, que é o thread da interface do usuário. Uma solução é capturar o contexto do thread no qual a corrotina foi chamada originalmente. Para fazer isso, instancie um objeto winrt::apartment_context, execute o trabalho em segundo plano e, em seguida, use 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 do thread da interface do usuário que criou o TextBlock, essa técnica funciona. Haverá muitos casos em seu aplicativo em que você tem certeza disso.
Para obter uma solução mais geral para atualizar a interface do usuário, que abrange os casos em que você não tem certeza sobre o thread que faz a chamada, você pode co_await a função winrt::resume_foreground função para alternar para um thread de primeiro plano específico. No exemplo de código abaixo, especificamos o thread em primeiro plano passando a fila do dispatcher associada ao TextBlock (acessando sua propriedade DispatcherQueue ). A implementação de winrt::resume_foreground chama DispatcherQueue.TryEnqueue nesse objeto de fila de despacho para executar o trabalho que vem depois dele 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 usa um parâmetro de prioridade opcional. Se você estiver usando esse parâmetro, o padrão mostrado acima será apropriado. Caso contrário, você pode optar por simplificar co_await winrt::resume_foreground(someDispatcherObject); em apenas co_await someDispatcherObject;.
Contextos de execução, retomada e alternância em uma corrotina
Falando genericamente, após um ponto de suspensão em uma corrotina, o thread original de execução pode desaparecer e a retomada pode ocorrer em qualquer thread (em outras palavras, qualquer thread pode chamar o método Completed para a operação assíncrona).
Porém, se você co_await qualquer um dos quatro tipos de operação assíncrona do Windows Runtime (IAsyncXxx), então C++/WinRT vai capturar o contexto de chamada no ponto que você co_await. E ele garante que você ainda esteja nesse contexto quando a continuação é retomada. O C++/ WinRT fará isso verificando se você já estiver no contexto da chamada e, se não estiver, alternando para ele. Se você estivesse em um thread STA (single-threaded apartment) antes de co_await, você estaria no mesmo posteriormente; se você estivesse em um thread MTA (multi-threaded apartment) antes de co_await, você estaria em um posteriormente.
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.
}
O motivo pelo qual você pode contar com esse comportamento é porque o C++/WinRT fornece código para adaptar esses tipos de operação assíncrona do Windows Runtime ao suporte à linguagem de corrotina C++ (esses trechos de código são chamados de adaptadores de espera). Os tipos awaitable restantes em C++/WinRT são simplesmente auxiliares e/ou wrappers de pool de thread, então eles completam o pool 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 você co_await algum outro tipo, até mesmo em uma implementação de corrotina C++/WinRT, outra biblioteca fornecerá os adaptadores e, em seguida, você precisará entender o que esses adaptadores fazem em termos de retomada e contextos.
Para manter as opções de contexto no mínimo, você pode usar algumas das técnicas que já vimos neste tópico. Vamos ver algumas ilustrações de fazer isso. Neste próximo exemplo de pseudocódigo, mostramos a estrutura de um manipulador de eventos que chama uma API do Windows Runtime para carregar uma imagem, passa para uma thread em segundo plano para processar essa imagem e, em seguida, retorna à thread da interface do usuário para exibir a imagem na interface do usuário.
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.
}
Para esse cenário, há um pouco de ineficiência em torno da chamada para StorageFile::OpenAsync. Há uma alternância de contexto necessária para um thread de segundo plano (de modo que o manipulador pode retornar a execução para o chamador), na continuação após a qual o C++/WinRT restaura o contexto do thread da interface do usuário. Mas, nesse caso, não é necessário estar na thread da UI até estarmos prestes a atualizar a UI. Quanto mais APIs do Windows Runtime chamarmos antes da chamada para winrt::resume_background, mais trocas de contexto de ida e volta desnecessárias teremos. A solução é não chamar nenhuma API do Windows Runtime antes disso. Mova todos para depois de 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 quiser fazer algo mais avançado, você poderá escrever seus próprios adaptadores await. Por exemplo, se quiser que um co_await continue no mesmo thread em que a ação assíncrona é concluída (portanto, não há nenhuma opção de contexto), você poderá começar escrevendo await adaptadores semelhantes aos mostrados abaixo.
Note
O exemplo de código abaixo é fornecido somente para fins educacionais; é para começar a entender como os adaptadores de espera funcionam. Se você quiser usar essa técnica em sua própria base de código, é recomendável que você desenvolva e teste seus próprios structs do adaptador await. Por exemplo, você pode escrever complete_on_any, complete_on_current e complete_on(dispatcher). Considere também transformá-los em templates que recebem o tipo IAsyncXxx como parâmetro de template.
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 entender como usar os adaptadores await no_switch, primeiro, você precisa saber que, quando o compilador de C++ se depara com uma expressão co_await, ele procura pelas funções chamadas await_ready, await_suspend e await_resume. A biblioteca C++/WinRT fornece essas funções para que você obtenha um comportamento razoável por padrão, assim.
IAsyncAction async{ ProcessFeedAsync() };
co_await async;
Para usar os adaptadores await no_switch, basta alterar o tipo dessa expressão co_await de IAsyncXxx para no_switch, deste modo.
IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);
Em seguida, em vez de procurar as três funções await_xxx que correspondem a IAsyncXxx, o compilador C++ procura funções que correspondam no_switch.
Uma análise mais aprofundada de winrt::resume_foreground
A partir de C++/WinRT 2.0, a função winrt::resume_foreground suspende a execução mesmo quando é chamada na thread do dispatcher (em versões anteriores, ela podia introduzir deadlocks em alguns cenários porque só suspendia se ainda não estivesse na thread do dispatcher).
O comportamento atual significa que você pode depender do desenrolamento e do re-enfileiramento da pilha, e isso é importante para a estabilidade do sistema, principalmente em código de sistemas de baixo nível. A última listagem de código na seção Programação pensada para afinidade de threads, acima, ilustra a realização de um pouco de cálculo complexo em um thread em segundo plano e a alternância para o thread de interface do usuário adequado a fim de atualizar a interface do usuário.
Veja a aparência de winrt::resume_foreground internamente.
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{ ... };
};
Esse comportamento atual, versus anterior, é análogo à diferença entre PostMessage e SendMessage no desenvolvimento de aplicativos Win32. PostMessage enfileira o trabalho e, em seguida, desenrola a pilha sem esperar que o trabalho seja concluído. O desenrolamento de pilha pode ser essencial.
A função winrt::resume_foreground originalmente suportava o CoreDispatcher (vinculado a um CoreWindow), que foi introduzido antes de Windows 10. Em aplicativos WinUI 3 e SDK de Aplicativos do Windows, use o DispatcherQueue em vez disso. Você pode criar um DispatcherQueue para suas próprias finalidades. Considere esta aplicação de console simples.
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 em um controlador) em um thread privado e, em seguida, passa o controlador para a corrotina. A corrotina pode usar a fila para aguardar (suspender e retomar) o thread privado. Outro uso comum de DispatcherQueue é criar uma fila no thread da interface do usuário atual para um aplicativo 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 };
}
Isso ilustra como você pode chamar e incorporar funções Win32 em seus projetos C++/WinRT, simplesmente chamando a função CreateDispatcherQueueController no estilo Win32 para criar o controlador e, em seguida, transferir a propriedade do controlador de fila resultante para o chamador como um objeto WinRT. Também é exatamente assim que você pode oferecer suporte a enfileiramento eficiente e sem interrupções no seu aplicativo de desktop Win32 existente, no 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 criando uma janela. Você pode imaginar que isso registra uma classe de janela e chama CreateWindow para criar a janela de área de trabalho de nível superior. A função CreateDispatcherQueueController é então chamada para criar o controlador de fila antes de chamar alguma corrotina com a fila de despacho pertencente a este controlador. Uma bomba de mensagem tradicional é inserida, na qual a retomada da corrotina ocorre naturalmente nesse thread. Tendo feito isso, você pode retornar ao mundo elegante de corrotinas para seu fluxo de trabalho baseado em mensagem ou assíncrono em seu aplicativo.
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 para winrt::resume_foreground sempre enfileirará e desenrolará a pilha. Opcionalmente, você também pode definir a prioridade de retomada.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
...
}
Ou, usando a ordem de enfileiramento padrão.
...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
co_await queue;
...
}
Note
Conforme mostrado acima, inclua o cabeçalho da projeção do namespace do tipo no qual você está realizando co_await. Por exemplo, Windows::System::DispatcherQueue ou Microsoft::UI::Dispatching::DispatcherQueue.
Ou, neste caso, detectar o encerramento da fila e lidar com isso de forma adequada.
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
...
if (co_await queue)
{
... // Resume on dispatcher thread.
}
else
{
... // Still on calling thread.
}
}
A co_await expressão retorna true, indicando que a retomada ocorrerá no thread do dispatcher. Em outras palavras, esse enfileiramento foi bem-sucedido. Por outro lado, ele retorna false para indicar que a execução permanece no thread de chamada porque o controlador da fila está sendo desligado e não está mais atendendo solicitações de fila.
Portanto, você tem muita potência a seu alcance quando combina o C++/WinRT com corrotinas e, principalmente, ao realizar um desenvolvimento de aplicativo de desktop no estilo Petzold tradicional.
Retornos de chamada de cancelamento e cancelar uma operação assíncrona
Os recursos do Windows Runtime para programação assíncrona permitem 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 arquivos e armazena o objeto de operação assíncrono resultante em um membro de dados. O usuário 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;
};
...
Quanto à 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 você executar o exemplo acima, verá ImplicitCancelationAsync imprimir uma mensagem por segundo por três segundos, após o qual ela terminará automaticamente como resultado de ser cancelada. Isso funciona porque, ao encontrar uma expressão co_await, uma corrotina verifica se ela foi cancelada. Em caso positivo ela deixa de funcionar e, caso contrário, ela suspende normalmente.
É claro que o cancelamento pode ocorrer enquanto a corrotina está suspensa. A corrotina só verifica se há cancelamento quando ela é retomada ou quando acessa outro co_await. Trata-se de um problema de latência com granulometria potencialmente muito grosseira ao responder ao cancelamento.
Portanto, outra opção é sondar explicitamente o cancelamento de dentro de sua corrotina. Atualize o exemplo acima com o código na listagem abaixo. Neste novo exemplo, ExplicitCancelationAsync recupera o objeto retornado pela função winrt::get_cancellation_token e o usa para verificar periodicamente se a coroutina foi cancelada. Desde que ela não tenha sido cancelada, a corrotina fará um loop indefinidamente; depois que ela tiver sido cancelada, o loop e a função se encerrarão normalmente. O resultado é o mesmo que o exemplo anterior, mas aqui sair acontece explicitamente e sob 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();
}
...
Aguardar winrt::get_cancellation_token recupera um token de cancelamento com conhecimento sobre a IAsyncAction que a corrotina está produzindo em seu nome. Você pode usar o operador de chamada de função nesse token para consultar o estado de cancelamento, essencialmente sondando para cancelamento. Se você estiver executando alguma operação associada à computação ou iterando por meio de uma coleção grande, essa é uma técnica razoável.
Registrar um retorno de chamada de cancelamento
O cancelamento do Windows Runtime não é propagado automaticamente para outros objetos assíncronos. Mas, introduzido na versão 10.0.17763.0 (Windows 10, versão 1809) do SDK do Windows, você pode registrar um retorno de chamada de cancelamento. Esse é um gancho preventivo pelo qual o cancelamento pode ser propagado e possibilita a integração com bibliotecas de simultaneidade existentes.
Neste próximo exemplo de código, NestedCoroutineAsync faz o trabalho, mas não tem nenhuma lógica de cancelamento especial. CancelationPropagatorAsync é essencialmente um wrapper para a corrotina aninhada; o wrapper encaminha o cancelamento preventivamente.
// 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 registra uma função lambda para seu próprio retorno de chamada de cancelamento e, em seguida, aguarda (suspende) até que o trabalho aninhado seja concluído. Quando ou se CancellationPropagatorAsync for cancelado, ele propagará o cancelamento para a corrotina aninhada. Não é necessário sondar para cancelamento. O cancelamento não é bloqueado indefinidamente. Esse mecanismo é flexível o suficiente para que você possa usá-lo para fornecer interoperabilidade com uma biblioteca de corrotina ou simultaneidade sem conhecimento de C++/WinRT.
Relatando progresso
Se a sua corrotina retornar um IAsyncActionWithProgress ou IAsyncOperationWithProgress, você poderá recuperar o objeto retornado pela função winrt::get_progress_token e usá-lo para relatar o progresso para um manipulador 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 relatar o andamento, invoque o token de andamento com o valor de andamento como argumento. Para definir um resultado provisório, use o método set_result() no token de progresso.
Note
Relatar resultados provisórios requer C++/WinRT versão 2.0.210309.3 ou posterior.
O exemplo acima opta por definir um resultado provisório para cada relatório de progresso. Se desejar, você poderá optar por relatar resultados provisórios a qualquer momento. Ele não precisa ser acoplado a um relatório de progresso.
Note
Não é correto implementar mais de um manipulador de conclusão para uma ação ou operação assíncrona. Você pode ter um único delegado para seu evento concluído ou pode usar co_await. Se você tiver os dois, o segundo falhará. Um dos dois tipos de manipuladores de conclusão a seguir é 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 obter mais informações sobre manipuladores de conclusão, consulte Tipos de delegado para ações e operações assíncronas.
Disparar e esquecer
Às vezes, você tem uma tarefa que pode ser feita simultaneamente com outro trabalho e não precisa aguardar a conclusão dessa tarefa (nenhum outro trabalho depende dela), nem precisa dela para retornar um valor. Nesse caso, você pode disparar a tarefa e esquecê-la. Você pode fazer isso escrevendo uma corrotina cujo tipo de retorno é winrt::fire_and_forget (em vez de um dos tipos de operação assíncrona 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 o tipo de retorno do manipulador de eventos quando você precisa executar operações assíncronas nele. Veja um exemplo (veja também referências fortes e fracas no 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 sender) fica sem nome, porque nunca o usamos. Por esse motivo, podemos deixá-lo como referência. Mas observe que args é passado por valor. Consulte a seção Passagem de parâmetros acima.
Como aguardar um identificador de kernel
O C++/WinRT fornece uma função winrt::resume_on_signal , que você pode usar para suspender até que um evento de kernel seja sinalizado. Você é responsável por garantir que o identificador permaneça válido até o retorno de co_await resume_on_signal(h).
resume_on_signal em si não pode fazer isso por você, porque você pode ter perdido o identificador antes mesmo do início do resume_on_signal , 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 de entrada é válido somente até a função retornar e essa função (que é uma corrotina) é retornada no primeiro ponto de suspensão (o primeiro co_await, nesse caso). Enquanto aguardava DoWorkAsync, o controle foi retornado ao autor da chamada, o quadro de chamada saiu do escopo e você não sabe mais se o identificador será válido quando a corrotina for retomada.
Tecnicamente, nossa corrotina está recebendo seus parâmetros por valor, como deveria (confira Passagem de parâmetro acima). Mas, neste caso, precisamos ir mais longe para que estejamos seguindo o espírito dessa orientação (em vez de apenas a carta). Precisamos passar uma referência forte (em outras palavras, propriedade) junto com o identificador. Veja aqui como fazer isso.
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 fornece uma semântica de propriedade, o que garante que o identificador do kernel permaneça válido durante o tempo de vida da corrotina.
Veja como você pode chamar essa 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.
}
Você pode passar um valor de tempo limite para 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");
}
Tempos limite assíncronos simplificados
O C++/WinRT é investido pesadamente em corrotinas do C++. Seu efeito na escrita de código de simultaneidade é transformacional. Esta seção aborda casos em que os detalhes da assincronia não são importantes, e tudo o que você quer é o resultado na hora. Por esse motivo, a implementação do IAsyncAction Windows Runtime interface de operação assíncrona do C++/WinRT tem uma função get, semelhante à fornecida por 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 é finalizado. Objetos assíncronos tendem a ser de curta duração, portanto, isso geralmente é tudo o que você precisa.
Mas há casos em que isso não é suficiente, e você precisa desistir de esperar depois que algum tempo tiver decorrido. 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 fornecendo a função wait_for . Ele também é implementado no IAsyncAction e, novamente, é semelhante ao fornecido por 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::d uration na interface, mas é limitado a algum intervalo menor que o que std::chrono::d uration fornece (aproximadamente 49,7 dias).
O wait_for neste próximo exemplo aguarda cerca de cinco segundos e verifica a conclusão. Se a comparação for favorável, você saberá que o objeto assíncrono foi concluído com êxito e pronto. Se você estiver aguardando algum resultado, poderá simplesmente seguir isso com uma chamada para o método GetResults para recuperar o resultado.
Note
wait_for e get são mutuamente exclusivos (você não pode invocar os dois). Cada um deles conta como um objeto de espera, e as ações/operações assíncronas do Windows Runtime oferecem suporte a apenas um único objeto de espera.
int main()
{
IAsyncOperation<int> async = ...
if (async.wait_for(5s) == AsyncStatus::Completed)
{
printf("result %d\n", async.GetResults());
}
}
Como o objeto assíncrono foi concluído até lá, o método GetResults retorna o resultado imediatamente, sem qualquer espera adicional. Como você pode ver, wait_for retorna o estado do objeto assíncrono. Então, você pode usá-lo para ter um controle mais detalhado, assim.
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 de que AsyncStatus::Completed significa que o objeto assíncrono foi concluído com êxito e você pode chamar o método GetResults para recuperar qualquer resultado.
- AsyncStatus::Canceled significa que o objeto assíncrono foi cancelado. Normalmente, um cancelamento é solicitado pelo chamador, portanto, seria raro lidar com esse estado. Normalmente, um objeto assíncrono cancelado é simplesmente descartado. Você pode chamar o método GetResults para relançar a exceção de cancelamento, se desejar.
- AsyncStatus::Error significa que o objeto assíncrono falhou de alguma forma. Você pode chamar o método GetResults para relançar a exceção, se desejar.
- AsyncStatus::Started significa que o objeto assíncrono ainda está em execução. O padrão assíncrono do Windows Runtime não permite várias esperas nem waiters. Isso significa que você não pode chamar wait_for em um loop. Se o tempo de espera tiver efetivamente expirado, restarão algumas opções. Você pode abandonar o objeto ou sondar seu status antes de chamar o método GetResults para recuperar qualquer resultado. Mas é melhor apenas descartar o objeto neste momento.
Um padrão alternativo é verificar apenas se foi Iniciado e permitir que GetResults lide com os 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();
}
Retornando uma matriz de forma assíncrona
Veja abaixo um exemplo de MIDL 3.0 que produz o erro MIDL2025: [msg]erro de sintaxe [contexto]: esperando > ou próximo de "[".
Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();
O motivo é que é inválido usar uma matriz como um argumento de tipo de parâmetro para uma interface parametrizada. Portanto, precisamos de uma maneira menos óbvia de atingir esse objetivo de retornar um array de forma assíncrona a partir de um método de uma classe de runtime.
Você pode retornar o vetor encapsulado em um objeto PropertyValue. O código de chamada cancela sua demarcação. Aqui está um exemplo de código, que você pode experimentar adicionando a classe de tempo de execução SampleComponent a um projeto Componente de Tempo de Execução do Windows (C++/WinRT) e, em seguida, consumindo-o de (por exemplo) um projeto Aplicativo em Branco, Empacotado (WinUI 3 na Área 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
- IAsyncActionWithProgress<TProgress> interface
- IAsyncOperation<TResult> interface
- IAsyncOperationWithProgress<TResult, TProgress> interface
- Método SyndicationClient::RetrieveFeedAsync
- winrt::fire_and_forget
- winrt::get_cancellation_token
- winrt::get_progress_token
- winrt::resume_foreground
Tópicos relacionados:
Windows developer