Partilhar via


Simultaneidade avançada e assincronia com C++/WinRT

Este tópico descreve cenários avançados com simultaneidade e assincronia em C++/WinRT.

Para uma introdução a este assunto, leia primeiro Simultaneidade e operações assíncronas.

Descarregando o trabalho no pool de threads do Windows

Uma corrotina é uma função como qualquer outra, onde o chamador é bloqueado até que a função lhe devolva a execução. E, a primeira oportunidade para uma co-rotina voltar é a primeira co_await, co_returnou co_yield.

Portanto, antes de realizar uma tarefa de computação intensiva numa corrotina, é necessário devolver a execução ao chamador (ou seja, introduzir um ponto de suspensão) de forma a garantir que o chamador não fique bloqueado. Se ainda não estiveres a fazer isso por co_await alguma outra operação, então podes co_await a função winrt::resume_background. Isso retorna o controlo para o chamador e retoma imediatamente a execução num grupo de threads.

O pool de threads que está sendo usado na implementação é o pool de threads de baixo nível do Windows, portanto, é otimamente 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 de threads

Este cenário expande-se em relação ao anterior. Você descarrega algum trabalho no pool de threads, mas deseja 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 , porque um TextBlock deve ser atualizado a partir do thread que o criou, que é o thread da interface do usuário. Uma solução é capturar o contexto de thread dentro do qual nossa co-rotina foi originalmente chamada. Para fazer isso, instancie um objeto winrt::apartment_context, faça trabalho em segundo plano e, em seguida, use o co_await para retornar 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 corotina acima seja chamada a partir 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 casos em que você não tem certeza sobre o thread de chamada, você pode co_await usar a função winrt::resume_foreground para alternar para um thread de primeiro plano específico. No exemplo de código abaixo, especificamos o thread de primeiro plano passando o objeto dispatcher associado ao TextBlock (acessando sua propriedade Dispatcher ). A implementação de winrt::resume_foreground chama CoreDispatcher.RunAsync nesse objeto dispatcher para executar o trabalho que vem depois dele na co-rotina.

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

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 é apropriado. Se não, então 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

Em termos gerais, após um ponto de suspensão em uma coroutine, a execução original pode desaparecer e a continuação pode ocorrer em qualquer fio (em outras palavras, qualquer fio pode chamar o método Completed para a operação assíncrona).

Mas se você utilizar algum dos quatro tipos de operação assíncrona do Tempo de Execução do Windows (co_await), então o C++/WinRT captura o contexto da chamada no ponto em que você . E garante que você ainda esteja nesse contexto quando a continuação for retomada. O C++/WinRT faz isso verificando se já está no contexto de chamada e, caso contrário, mudando para ele. Se estivesse num apartamento single-threaded (STA) antes de co_await, então estará no mesmo depois; se estivesse num apartamento multi-threaded (MTA) antes de co_await, então estará num 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 você pode confiar nesse comportamento é que o C++/WinRT fornece código para adaptar esses tipos de operação assíncrona do Windows Runtime ao suporte à linguagem de co-rotina do C++ (essas partes de código são chamadas de adaptadores de espera). Os tipos de espera restantes em C++/WinRT são simplesmente encapsuladores do pool de threads e/ou auxiliares; por isso, são executados no 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, mesmo dentro de uma implementação de co-rotina C++/WinRT, outra biblioteca fornece os adaptadores e você precisará entender o que esses adaptadores fazem em termos de retomada e contextos.

Para manter as alternâncias de contexto reduzidas ao mínimo, você pode usar algumas das técnicas que já vimos neste tópico. Vamos ver algumas ilustrações disso. Neste próximo exemplo de pseudocódigo, apresentamos a estrutura de um manipulador de eventos que chama uma API do Windows Runtime para carregar uma imagem, utiliza um fio de plano de fundo para processar essa imagem e retorna ao fio da interface para exibir 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->Dispatcher());

    // 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 mudança de contexto necessária para um thread em segundo plano (para que o manipulador possa retornar a execução ao chamador), na retomada 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 no thread da interface do usuário até que estejamos prestes a atualizá-la. Quanto mais APIs do Tempo de Execução do Windows chamamos antes de nossa chamada para winrt::resume_background, mais alternâncias de contexto desnecessárias incorremos. A solução não é chamar nenhuma API do Tempo de Execução do Windows antes disso. Mova-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->Dispatcher());

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

    // Display the image in the UI.
}

Se quiser fazer algo mais avançado, pode escrever os seus próprios adaptadores de 'await'. Por exemplo, se tu quiseres que um co_await seja retomado no mesmo fio em que a ação assíncrona é concluída (portanto, não há troca de contexto), então poderias começar por escrever adaptadores await semelhantes aos mostrados abaixo.

Observação

O exemplo de código abaixo é fornecido apenas para fins educacionais; É para você 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, recomendamos que você desenvolva e teste sua(s) própria(s) estrutura(s) do adaptador. Por exemplo, você pode escrever complete_on_any, complete_on_current e complete_on(dispatcher). Considere também torná-los modelos que usam o tipo IAsyncXxx como um 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 entender como usar os adaptadores no_switch await , primeiro você precisa saber que, quando o compilador C++ encontra uma co_await expressão, ele procura 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, como este.

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

Para usar os adaptadores await no_switch, basta alterar o tipo desta co_await expressão de IAsyncXxx para no_switch, desta forma.

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 a no_switch.

Um mergulho mais profundo em winrt::resume_foreground

A partir do C++/WinRT 2.0, a função winrt::resume_foreground é suspensa mesmo se for chamada a partir do thread do dispatcher (em versões anteriores, ela podia introduzir deadlocks em alguns cenários porque só era suspensa se ainda não estivesse no thread do dispatcher).

O comportamento atual significa que se pode confiar no desenrolamento da pilha e no reencaminhamento a ocorrer; e é importante para a estabilidade do sistema, especialmente no código de sistemas de baixo nível. A última listagem de código na seção Programação com afinidade de thread em mente, acima, ilustra a execução de alguns cálculos complexos em um thread em segundo plano e, em seguida, alternar para o thread de interface do usuário apropriado para atualizar a interface do usuário (UI).

Veja como winrt::resume_foreground funciona 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 da pilha pode ser essencial.

A função winrt::resume_foreground inicialmente só suportava o CoreDispatcher (ligado a umCoreWindow ), que foi introduzido antes do Windows 10. Desde então, introduzimos um dispatcher mais flexível e eficiente: o DispatcherQueue. Você pode criar um DispatcherQueue para seus próprios propósitos. Considere este aplicativo 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) num fio privado e, em seguida, passa o controlador para a corrotina. A co-rotina pode usar a fila para aguardar (suspender e retomar) no thread privado. Outro uso comum de DispatcherQueue é criar uma fila no thread atual da interface do usuário para uma área de trabalho tradicional ou aplicativo 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 precisamente assim que pode suportar filas de espera de forma eficiente e integrada na sua aplicação de ambiente de trabalho 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 simples função principal começa com a criação de uma janela. Você pode imaginar que isso registra uma classe de janela e chama CreateWindow para criar a janela da área de trabalho de nível superior. A função CreateDispatcherQueueController é então chamada para criar o controlador de fila antes de chamar alguma co-rotina com a fila do dispatcher de propriedade desse controlador. Uma bomba de mensagem tradicional é então inserida onde a retomada da co-rotina ocorre naturalmente neste tópico. Feito isso, você pode retornar ao elegante mundo das co-rotinas para seu fluxo de trabalho assíncrono ou baseado em mensagens 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 irá sempre enfileirar e, depois, 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;
 
    ...
}

Observação

Como se vê acima, certifique-se de incluir o cabeçalho de projeção para o namespace do tipo que está a usar no co_await. Por exemplo, Windows::UI::Core::CoreDispatcher, Windows::System::DispatcherQueue ou Microsoft::UI::Dispatching::DispatcherQueue.

Ou, neste caso, a detecção do encerramento da fila e a lidagem de forma adequada.

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 retorna true, indicando que a retomada ocorrerá no thread do dispatcher. Em outras palavras, essa fila foi bem-sucedida. 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 às solicitações da fila.

Assim, você tem um grande poder na ponta dos dedos quando combina C++/WinRT com co-rotinas; e especialmente ao fazer algum desenvolvimento de aplicativos de desktop no estilo Petzold da velha escola.

Cancelar uma operação assíncrona e cancelar retornos de chamada

Os recursos do Tempo de Execução do Windows para programação assíncrona permitem cancelar uma ação ou operação assíncrona em voo. Este é um exemplo que chama StorageFolder::GetFilesAsync para recuperar uma coleção potencialmente grande de arquivos e armazena o objeto de operação assíncrona 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 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;
};
...

Para a implementação do cancelamento, vamos começar por 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 termina automaticamente como resultado de ser cancelada. Isso funciona porque, ao encontrar uma co_await expressão, uma corrotina analisa se foi cancelada. Se tiver, então entra em curto-circuito; e se não tiver, suspende normalmente.

O cancelamento pode, claro, acontecer enquanto a co-rotina estiver suspensa. Só quando a cortina retoma ou atinge outro co_awaité que verifica o cancelamento. A questão é de latência potencialmente demasiado granular na resposta ao cancelamento.

Assim, outra opção é pesquisar explicitamente o cancelamento de dentro da sua co-rotina. 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 usa-o para verificar de tempos a tempos se a corrotina foi cancelada. Desde que não seja cancelada, a corrotina circula indefinidamente; uma vez 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 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();
}
...

Waiting on winrt::get_cancellation_token recupera um token de cancelamento com conhecimento do IAsyncAction que a corrotina está produzindo em seu benefício. Você pode usar o operador de chamada de função nesse token para consultar o estado de cancelamento — essencialmente, sondagem para cancelamento. Se você estiver executando alguma operação vinculada à computação ou iterando através de uma grande coleção, então esta é uma técnica razoável.

Definir uma função de retorno de cancelamento

O cancelamento no Windows Runtime não se propaga automaticamente para outros objetos assíncronos. Mas, introduzido na versão 10.0.17763.0 (Windows 10, versão 1809) do SDK do Windows, pode-se registar um callback de cancelamento. Este é um gancho preventivo pelo qual o cancelamento pode ser propagado e torna possível 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 nele. CancelationPropagatorAsync é essencialmente um envoltório na co-rotina aninhada; o envoltório encaminha antecipadamente o cancelamento.

// 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 é cancelado, ele propaga o cancelamento para a corrotina aninhada. Não há necessidade de sondagem para cancelamento; O cancelamento também não é bloqueado indefinidamente. Esse mecanismo é flexível o suficiente para você usá-lo para interoperar com uma biblioteca de co-rotina ou simultaneidade que não sabe nada de C++/WinRT.

Comunicação dos progressos realizados

Se a sua co-rotina retornar IAsyncActionWithProgress ou IAsyncOperationWithProgress, podes recuperar o objeto retornado pela função winrt::get_progress_token e usá-lo para reportar o 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 relatar o progresso, invoque o token de progresso com o valor de progresso como argumento. Para definir um resultado provisório, use o método set_result() no token de progresso.

Observação

O relatório de 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. Pode optar por comunicar os resultados provisórios a qualquer momento, se for o caso. Não precisa de ser acompanhada de um relatório intercalar.

Observação

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 o seu evento concluído ou pode co_await-lo. Se você tiver ambos, então o segundo falhará. Qualquer 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 saber mais sobre manipuladores de conclusão, veja Tipos de delegados para ações e operações assíncronas.

Fogo e esquecimento

Às vezes, você tem uma tarefa que pode ser feita simultaneamente com outro trabalho, e você não precisa esperar que essa tarefa seja concluída (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 co-rotina cujo tipo de retorno é winrt::fire_and_forget (em vez de um dos tipos de operação assíncrona do Tempo de Execução do Windows 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. 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 remetente) não é nomeado, porque nunca o usamos. Por essa razão, podemos deixá-lo como referência com segurança. Mas observe que args é passado por valor. Consulte a seção Passagem de parâmetros acima.

Aguardando um identificador do kernel

C++/WinRT fornece uma função winrt::resume_on_signal , que você pode usar para suspender até que um evento do kernel seja sinalizado. Você é responsável por garantir que o identificador permaneça válido até que seu co_await resume_on_signal(h) retorne. resume_on_signal si não pode fazer isso por você, porque você pode ter perdido a alça antes mesmo do 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 de entrada é válido apenas até que a função retorne, e esta função (que é uma co-rotina) retorna no primeiro ponto de suspensão (o primeiro co_await neste caso). Enquanto aguarda o DoWorkAsync, o controle retornou ao chamador, o quadro de chamada saiu do escopo e você não sabe mais se o identificador será válido quando sua co-rotina for retomada.

Tecnicamente, a nossa co-rotina está a receber os seus parâmetros por valor, como deveria (veja Passagem de parâmetros acima). Mas, neste caso, precisamos dar um passo adiante para que estejamos seguindo o espírito dessa orientação (e não apenas a letra). Precisamos passar uma referência forte (em outras palavras, propriedade) junto com a alça. 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 proporciona semântica de propriedade, garantindo que o identificador do kernel se mantenha válido durante a vida útil da corrotina.

Veja como você pode chamar isso de corotina.

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 de espera assíncronos simplificados

C++/WinRT investe fortemente em corrotinas C++. O seu efeito na escrita de código de concorrência é transformador. Esta seção discute casos em que os detalhes da asincronia não são importantes, e tudo o que quer é o resultado na mesma hora. Por esse motivo, a implementação do C++/WinRT da interface de operação assíncrona do Windows Runtime IAsyncAction 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, então isso geralmente é tudo o que você precisa.

Mas há casos em que isso não é suficiente, e você precisa abandonar a espera depois de algum tempo. Escrever esse código sempre foi possível, graças aos blocos de construção fornecidos pelo Tempo de Execução do Windows. Mas agora o C++/WinRT torna tudo muito mais fácil, fornecendo a função wait_for . Também é implementado em 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");
    }
}

Observação

wait_for usa std::chrono::d uration na interface, mas é limitado a algum intervalo menor do 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, em seguida, verifica a conclusão. Se a comparação for favorável, então você sabe que o objeto assíncrono foi concluído com êxito e pronto. Se você estiver esperando por algum resultado, então você pode simplesmente seguir isso com uma chamada para o método GetResults para recuperar o resultado.

Observação

wait_for e get são mutuamente exclusivos (não se pode invocar os dois). Cada um deles conta como um garçom e as ações/operações assíncronas do Tempo de Execução do Windows oferecem suporte a apenas um único garçom.

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é então, o método GetResults retorna o resultado imediatamente, sem mais espera. Como você pode ver, wait_for retorna o estado do objeto assíncrono. Assim, você pode usá-lo para um controle mais refinado, 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 de que AsyncStatus::Completed significa que o objeto async foi concluído com êxito e você pode chamar o método GetResults para recuperar qualquer resultado.
  • AsyncStatus::Canceled significa que o objeto async foi cancelado. Um cancelamento é normalmente solicitado pelo chamador, por isso 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 async 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 async ainda está em execução. O padrão assíncrono do Tempo de Execução do Windows não permite várias esperas, nem esperadores. Isso significa que você não pode chamar wait_for em um loop. Se a espera tiver efetivamente expirado, então você terá algumas opções. Você pode abandonar o objeto ou pesquisar 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 o estado Iniciado e deixar que o GetResults trate 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

Abaixo está um exemplo de MIDL 3.0 que produz erro MIDL2025: [msg]erro de sintaxe [context]: esperando > ou, próximo a "[".

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 alcançar o objetivo de devolver de forma assíncrona uma matriz a partir de um método de classe de runtime.

Você pode retornar a matriz encaixotada em um objeto PropertyValue . Em seguida, o código de chamada desencaixota-o. Aqui está um exemplo de código, que você pode experimentar adicionando a classe de tempo de execução SampleComponent a um projeto do Componente do Tempo de Execução do Windows (C++/WinRT) e, em seguida, consumindo-o de (por exemplo) um projeto Core App (C++/WinRT).

// SampleComponent.idl
namespace MyComponentProject
{
    runtimeclass SampleComponent
    {
        Windows.Foundation.IAsyncOperation<IInspectable> RetrieveCollectionAsync();
    };
}

// SampleComponent.h
...
struct SampleComponent : SampleComponentT<SampleComponent>
{
    ...
    Windows::Foundation::IAsyncOperation<Windows::Foundation::IInspectable> RetrieveCollectionAsync()
    {
        co_return Windows::Foundation::PropertyValue::CreateInt32Array({ 99, 101 }); // Box an array into a PropertyValue.
    }
}
...

// SampleCoreApp.cpp
...
MyComponentProject::SampleComponent m_sample_component;
...
auto boxed_array{ co_await m_sample_component.RetrieveCollectionAsync() };
auto property_value{ boxed_array.as<winrt::Windows::Foundation::IPropertyValue>() };
winrt::com_array<int32_t> my_array;
property_value.GetInt32Array(my_array); // Unbox back into an array.
...

APIs importantes