C++/WinRT를 사용하는 고급 동시성 및 비동기

이 항목에서는 C++/WinRT의 동시성 및 비동기 고급 시나리오에 대해 설명합니다.

이 주제에 대한 소개는 먼저 동시성 및 비동기 작업을 읽어 줍니다.

Windows 스레드 풀에 작업 오프로드

코루틴은 함수가 실행을 반환할 때까지 호출자가 차단된다는 측면에서 다른 함수와 마찬가지로 함수입니다. 그리고 코루틴이 반환될 첫 번째 기회는 첫 번째 co_await, co_return 또는 co_yield입니다.

따라서 코루틴에서 컴퓨팅 바인딩된 작업을 수행하려면 호출자가 차단되지 않도록 호출자에게 실행을 반환해야 합니다(즉, 일시 중단 지점 도입). 이미 다른 작업을 co_await-ing하여 그렇게 하고 있는 것이 아니라면, co_awaitwinrt::resume_background 함수를 호출할 수 있습니다. 그러면 호출자에게 컨트롤을 반환한 다음 스레드 풀 스레드에서 실행을 즉시 다시 시작합니다.

구현에 사용되는 스레드 풀은 낮은 수준의 Windows 스레드 풀이므로 최적으로 효율적입니다.

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

스레드 선호도를 염두에 둔 프로그래밍

이 시나리오는 이전 시나리오에서 확장됩니다. 일부 작업을 스레드 풀에 오프로드한 다음 UI(사용자 인터페이스)에 진행률을 표시하려고 합니다.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    textblock.Text(L"Done!"); // Error: TextBlock has thread affinity.
}

위의 코드는 UI 스레드인 TextBlock을 만든 스레드에서 업데이트해야 하므로 winrt::hresult_wrong_thread 예외를 throw합니다. 한 가지 해결 방법은 코루틴이 원래 호출된 스레드 컨텍스트를 캡처하는 것입니다. 이렇게 하려면 winrt::apartment_context 개체를 인스턴스화하고 co_await 백그라운드 작업을 수행하고 apartment_context 호출 컨텍스트로 다시 전환합니다.

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    winrt::apartment_context ui_thread; // Capture calling context.

    co_await winrt::resume_background();
    // Do compute-bound work here.

    co_await ui_thread; // Switch back to calling context.

    textblock.Text(L"Done!"); // Ok if we really were called from the UI thread.
}

위의 코루틴이 TextBlock을 만든 UI 스레드에서 호출되는 한 이 기술이 작동합니다. 앱에서 그것을 확신할 수 있는 경우가 많을 것입니다.

호출 스레드에 대해 확실하지 않은 경우를 다루는 UI를 업데이트하는 보다 일반적인 솔루션의 경우 co_await 함수를 사용하여 특정 포그라운드 스레드로 전환할 수 있습니다. 아래 코드 예제에서는 TextBlock 과 연결된 디스패처 큐를 전달하여 포그라운드 스레드를 지정합니다( DispatcherQueue 속성에 액세스). winrt::resume_foreground 구현은 해당 디스패처 큐 개체에서 DispatcherQueue.TryEnqueue를 호출하여 코루틴에서 그 후에 오는 작업을 실행합니다.

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

winrt::resume_foreground 함수는 선택적 우선 순위 매개 변수를 취합니다. 해당 매개 변수를 사용하는 경우 위에 표시된 패턴이 적절합니다. 그렇지 않다면 co_await winrt::resume_foreground(someDispatcherObject);을(를) co_await someDispatcherObject;만으로 단순화하도록 선택할 수 있습니다.

코루틴에서 실행 컨텍스트, 다시 시작 및 전환

대체로 코루틴의 일시 중단 지점이 지나면 원래 실행 스레드가 사라지고 다시 시작이 모든 스레드에서 발생할 수 있습니다(즉, 모든 스레드가 비동기 작업에 대해 Completed 메서드를 호출할 수 있음).

그러나 네 가지 Windows 런타임 비동기 작업 형식(IAsyncXxx) 중 하나를 co_await하면, C++/WinRT는 co_await하는 시점의 호출 컨텍스트를 캡처합니다. 또한 후속 작업이 다시 시작될 때도 그 컨텍스트를 계속 유지한 상태를 보장합니다. C++/WinRT는 호출 컨텍스트에 이미 있는지 확인하고 그렇지 않은 경우 해당 컨텍스트로 전환하여 이 작업을 수행합니다. co_await 이전에 단일 스레드 아파트(STA) 스레드에 있었다면 이후에도 동일한 스레드에 있게 되며, co_await 이전에 다중 스레드 아파트(MTA) 스레드에 있었다면 이후에도 다중 스레드 아파트(MTA) 스레드에 있게 됩니다.

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

이 동작을 사용할 수 있는 이유는 C++/WinRT가 이러한 Windows 런타임 비동기 작업 유형을 C++ 코루틴 언어 지원에 맞게 조정하는 코드를 제공하기 때문입니다(이러한 코드 조각은 대기 어댑터라고 함). C++/WinRT의 나머지 대기 가능 형식은 단순히 스레드 풀 래퍼 및/또는 도우미입니다. 스레드 풀에서 완료되도록 합니다.

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

C++/WinRT 코루틴 구현 내에서도 다른 형식을 사용하는 경우 co_await 다른 라이브러리가 어댑터를 제공하므로 해당 어댑터가 다시 시작 및 컨텍스트 측면에서 수행하는 작업을 이해해야 합니다.

컨텍스트 전환을 최소한으로 유지하려면 이 항목에서 이미 본 기술 중 일부를 사용할 수 있습니다. 이 작업을 수행하는 몇 가지 일러스트레이션을 살펴보겠습니다. 다음 의사 코드 예제에서는 Windows 런타임 API를 호출하여 이미지를 로드하고, 백그라운드 스레드에 드롭하여 해당 이미지를 처리한 다음, UI 스레드로 돌아와 UI에 이미지를 표시하는 이벤트 처리기의 개요를 보여 줍니다.

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

이 시나리오에서는 StorageFile::OpenAsync 호출과 관련하여 약간의 비효율성이 있습니다. C++/WinRT가 UI 스레드 컨텍스트를 복원한 후 다시 시작 시 처리기가 호출자에게 실행을 반환할 수 있도록 백그라운드 스레드로 필요한 컨텍스트 전환이 있습니다. 그러나 이 경우 UI를 업데이트하기 전까지는 UI 스레드에 있을 필요가 없습니다. winrt::resume_background 호출하기 전에 호출하는 Windows 런타임 API가 많을수록 불필요한 전후 컨텍스트 전환이 발생합니다. 솔루션은 그 전에 Windows 런타임 API를 호출하지 않습니다. 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.
}

고급 작업을 수행하려면 사용자 고유의 await 어댑터를 작성할 수 있습니다. 예를 들어 비동기 작업이 완료된 것과 동일한 스레드에서 다시 시작하려는 co_await 경우(컨텍스트 전환이 없음) 아래에 표시된 것과 유사한 await 어댑터를 작성하여 시작할 수 있습니다.

메모

아래 코드 예제는 교육용으로만 제공됩니다. await 어댑터의 작동 방식을 이해하기 시작합니다. 사용자 고유의 코드베이스에서 이 기술을 사용하려면 사용자 고유의 await 어댑터 구조체를 개발하고 테스트하는 것이 좋습니다. 예를 들어 complete_on_any, complete_on_current, 및 complete_on(dispatcher)를 작성할 수 있습니다. 또한 IAsyncXxx 형식을 템플릿 매개 변수로 사용하는 템플릿을 만드는 것이 좋습니다.

struct no_switch
{
    no_switch(Windows::Foundation::IAsyncAction const& async) : m_async(async)
    {
    }

    bool await_ready() const
    {
        return m_async.Status() == Windows::Foundation::AsyncStatus::Completed;
    }

    void await_suspend(std::experimental::coroutine_handle<> handle) const
    {
        m_async.Completed([handle](Windows::Foundation::IAsyncAction const& /* asyncInfo */, Windows::Foundation::AsyncStatus const& /* asyncStatus */)
        {
            handle();
        });
    }

    auto await_resume() const
    {
        return m_async.GetResults();
    }

private:
    Windows::Foundation::IAsyncAction const& m_async;
};

no_switch await 어댑터를 사용하는 방법을 이해하려면 먼저 C++ 컴파일러가 co_await 식을 마주치면 await_ready, await_suspend, 그리고 await_resume라는 함수를 찾는다는 점을 알아야 합니다. C++/WinRT 라이브러리는 이러한 함수를 제공하므로 다음과 같이 기본적으로 적절한 동작을 얻을 수 있습니다.

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

no_switch await 어댑터를 사용하려면 다음과 같이 해당 식의 형식을 co_awaitIAsyncXxx에서 no_switch 변경하면 됩니다.

IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);

그런 다음 IAsyncXxx에 대응하는 세 개의 await_xxx 함수를 찾는 대신, C++ 컴파일러는 no_switch와 일치하는 함수를 찾게 됩니다.

winrt에 대한 심층 분석 ::resume_foreground

C++/WinRT 2.0부터 winrt::resume_foreground 함수는 디스패처 스레드에서 호출된 경우에도 일시 중단됩니다(이전 버전에서는 디스패처 스레드에 아직 없는 경우에만 일시 중단되었기 때문에 일부 시나리오에서 교착 상태가 발생할 수 있음).

현재와 같은 동작 방식은 스택 언와인딩과 재대기열 추가가 이루어진다는 점을 신뢰할 수 있음을 뜻하며, 이는 시스템 안정성에 중요하고 특히 저수준 시스템 코드에서 더욱 그렇습니다. 스레드 선호도를 염두에 두고 프로그래밍 섹션의 마지막 코드 목록은 백그라운드 스레드에서 복잡한 계산을 수행한 다음 UI(사용자 인터페이스)를 업데이트하기 위해 적절한 UI 스레드로 전환하는 방법을 보여 줍니다.

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

이 현재 동작과 이전 동작은 Win32 애플리케이션 개발에서 PostMessageSendMessage 의 차이점과 유사합니다. PostMessage는 작업을 대기열에 넣은 다음 작업이 완료되기를 기다리지 않고 호출 스택을 되돌립니다. 스택 해제는 필수일 수 있습니다.

winrt::resume_foreground 함수는 원래 Windows 10 전에 도입된 CoreDispatcher(CoreWindow에 연결됨)를 지원했습니다. WinUI 3 및 Windows 앱 SDK 앱에서 DispatcherQueue를 대신 사용합니다. 자신의 용도로 DispatcherQueue 를 만들 수 있습니다. 이 간단한 콘솔 애플리케이션을 고려합니다.

using namespace Windows::System;

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    auto controller{ DispatcherQueueController::CreateOnDedicatedThread() };
    RunAsync(controller.DispatcherQueue());
    getchar();
}

위의 예제에서는 프라이빗 스레드에 큐(컨트롤러 내에 포함)를 만든 다음, 컨트롤러를 코루틴에 전달합니다. 코루틴은 큐를 사용하여 프라이빗 스레드에서 대기(일시 중단 및 다시 시작)할 수 있습니다. DispatcherQueue의 또 다른 일반적인 용도는 기존 데스크톱 또는 Win32 앱에 대한 현재 UI 스레드에 큐를 만드는 것입니다.

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

이는 Win32 스타일 CreateDispatcherQueueController 함수를 호출하여 컨트롤러를 만든 다음 결과 큐 컨트롤러의 소유권을 WinRT 개체로 호출자에게 전송하여 Win32 함수를 호출하고 C++/WinRT 프로젝트에 통합하는 방법을 보여 줍니다. 이것이야말로 기존의 Petzold 스타일 Win32 데스크톱 애플리케이션에서도 효율적이고 원활한 큐 처리를 지원할 수 있는 정확한 방법입니다.

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);
    }
}

위의 간단한 함수는 창을 만들어 시작합니다. 창 클래스를 등록하고 CreateWindow 를 호출하여 최상위 데스크톱 창을 만드는 것을 상상할 수 있습니다. CreateDispatcherQueueController 함수는 이 컨트롤러가 소유한 디스패처 큐를 사용하여 일부 코루틴을 호출하기 전에 큐 컨트롤러를 만들기 위해 호출됩니다. 그런 다음 이 스레드에서 코루틴의 재개가 자연스럽게 발생하는 기존 메시지 펌프가 입력됩니다. 이렇게 하면 애플리케이션 내에서 비동기 또는 메시지 기반 워크플로에 대한 우아한 코루틴 세계로 돌아갈 수 있습니다.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ... // Begin on the calling thread...
 
    co_await winrt::resume_foreground(queue);
 
    ... // ...resume on the dispatcher thread.
}

winrt::resume_foreground 호출은 항상 큐에 대기한 다음 스택을 해제합니다. 필요에 따라 다시 시작 우선 순위를 설정할 수도 있습니다.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
 
    ...
}

또는 기본 큐 순서를 사용하거나.

...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await queue;
 
    ...
}

메모

위와 같이, co_await 중인 형식이 속한 네임스페이스의 프로젝션 헤더를 반드시 포함해야 합니다. 예를 들어 Windows::System::DispatcherQueue 또는 Microsoft::UI::Dispatching::DispatcherQueue입니다.

또는 이 경우에는 큐 종료를 감지하고 이를 적절히 처리하는 것입니다.

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    if (co_await queue)
    {
        ... // Resume on dispatcher thread.
    }
    else
    {
        ... // Still on calling thread.
    }
}

co_await 식은 true을 반환하며, 이는 재개가 디스패처 스레드에서 이루어짐을 나타냅니다. 즉, 대기열에 추가하는 데 성공했습니다. 반대로, 큐의 컨트롤러가 종료 중이어서 더 이상 큐 요청을 처리하지 않으므로 실행이 호출한 스레드에서 계속됨을 나타내기 위해 false를 반환합니다.

따라서 C++/WinRT를 코루틴과 결합하면 손끝에서 많은 힘을 발휘할 수 있습니다. 특히 일부 구식 Petzold 스타일의 데스크톱 응용 프로그램 개발을 수행하는 경우.

비동기 작업 취소 및 취소 콜백

비동기 프로그래밍에 대한 Windows 런타임 기능을 사용하면 진행 중인 비동기 작업 또는 작업을 취소할 수 있습니다. StorageFolder::GetFilesAsync를 호출하여 잠재적으로 큰 파일 컬렉션을 검색하고 결과 비동기 작업 개체를 데이터 멤버에 저장하는 예제는 다음과 같습니다. 사용자에게 작업을 취소할 수 있는 옵션이 있습니다.

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

취소의 구현 측면에서 간단한 예제로 시작해 보겠습니다.

// 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();
}

위의 예제를 실행하면 ImplicitCancelationAsync 가 3초 동안 초당 하나의 메시지를 인쇄하는 것을 볼 수 있으며, 그 후 취소된 결과로 자동으로 종료됩니다. 이는 코루틴이 co_await 표현식을 만나면 취소되었는지 확인하기 때문에 작동합니다. 그렇다면 즉시 빠져나가고, 그렇지 않다면 평소처럼 일시 중단됩니다.

물론 코루틴이 일시 중단되는 동안 취소가 발생할 수 있습니다. 코루틴이 재개되거나 다른 co_await에 도달하는 경우에만 취소 여부를 확인합니다. 문제는 취소 요청에 응답할 때 지연의 세분성이 충분히 세밀하지 않을 수 있다는 점입니다.

따라서 또 다른 옵션은 코루틴 내에서 취소 여부를 명시적으로 폴링하는 것입니다. 위의 예제를 아래 목록의 코드로 업데이트합니다. 이 새 예제에서 ExplicitCancelationAsyncwinrt::get_cancellation_token 함수에서 반환된 개체를 검색하고 이를 사용하여 코루틴이 취소되었는지 여부를 주기적으로 확인합니다. 취소되지 않는 한 코루틴은 무기한 반복됩니다. 취소되면 루프와 함수가 정상적으로 종료됩니다. 결과는 이전 예제와 동일하지만 여기서는 명시적으로 종료되고 제어됩니다.

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();
}
...

winrt::get_cancellation_token 대기하면 코루틴이 사용자 대신 생성되는 IAsyncAction에 대한 지식이 있는 취소 토큰을 검색합니다. 해당 토큰의 함수 호출 연산자를 사용하여 취소 상태를 쿼리할 수 있습니다( 기본적으로 취소를 위해 폴링). 컴퓨팅 바인딩된 작업을 수행하거나 큰 컬렉션을 반복하는 경우 이는 적절한 기술입니다.

취소 콜백 등록

Windows 런타임 취소는 다른 비동기 개체로 자동으로 전달되지 않습니다. 그러나 Windows SDK의 버전 10.0.17763.0(Windows 10, 버전 1809)에서 도입된 취소 콜백을 등록할 수 있습니다. 취소를 전파할 수 있는 선제 후크이며 기존 동시성 라이브러리와 통합할 수 있습니다.

다음 코드 예제에서 NestedCoroutineAsync 는 작업을 수행하지만 특별한 취소 논리는 없습니다. CancelationPropagatorAsync 는 기본적으로 중첩된 코루틴의 래퍼입니다. 래퍼는 취소를 선제적으로 전달합니다.

// 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 는 자체 취소 콜백에 대해 람다 함수를 등록한 다음 중첩된 작업이 완료될 때까지 대기합니다(일시 중단). CancellationPropagatorAsync가 취소되는 경우, 중첩된 코루틴에 취소를 전파합니다. 취소를 폴링할 필요가 없습니다. 취소가 무기한 차단되지도 않습니다. 이 메커니즘은 C++/WinRT를 전혀 모르는 코루틴 또는 동시성 라이브러리와 상호 운용하는 데 사용할 수 있을 만큼 유연합니다.

진행률 보고

코루틴이 IAsyncActionWithProgress 또는 IAsyncOperationWithProgress를 반환하는 경우 winrt::get_progress_token 함수에서 반환된 개체를 검색하여 진행률을 다시 진행률 처리기에 보고할 수 있습니다. 다음은 코드 예제입니다.

// 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();
}

진행률을 보고하려면 진행률 값을 인수로 사용하여 진행률 토큰을 호출합니다. 임시 결과를 설정하려면 진행률 토큰에 set_result() 메서드를 사용합니다.

메모

임시 결과를 보고하려면 C++/WinRT 버전 2.0.210309.3 이상이 필요합니다.

위의 예제에서는 모든 진행률 보고서에 대해 임시 결과를 설정하도록 선택합니다. 언제든지 임시 결과를 보고하도록 선택할 수 있습니다. 진행률 보고서와 결합할 필요는 없습니다.

메모

비동기 작업 또는 작업에 대해 둘 이상의 완료 처리기를 구현하는 것은 올바르지 않습니다. 완료 이벤트에 대해 하나의 대리자를 둘 수도 있고, 아니면 여기에 co_await할 수도 있습니다. 둘 다 있는 경우 두 번째가 실패합니다. 다음 두 종류의 완료 처리기 중 하나가 적합합니다. 둘 다 동일한 비동기 개체에 대한 것이 아닙니다.

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

완료 처리기에 대한 자세한 내용은 비동기 작업 및 작업에 대한 대리자 형식을 참조하세요.

발사 후 망각

경우에 따라 다른 작업과 동시에 수행할 수 있는 작업이 있으며 해당 작업이 완료될 때까지 기다릴 필요가 없으며(다른 작업은 작업에 의존하지 않음) 값을 반환할 필요가 없습니다. 이 경우 작업을 실행하고 잊어버릴 수 있습니다. 반환 형식이 winrt::fire_and_forget(Windows 런타임 비동기 작업 형식 또는 동시성::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 이벤트 처리기에서 비동기 작업을 수행해야 하는 경우 이벤트 처리기의 반환 형식으로도 유용합니다. 다음은 예제입니다( 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.
}

첫 번째 인수( 보낸 사람)는 절대 사용하지 않으므로 이름이 지정되지 않은 상태로 남아 있습니다. 이러한 이유로 참조로 남겨 두면 안전합니다. 그러나 args는 값으로 전달된다는 점에 유의하십시오. 위의 매개 변수 전달 섹션을 참조하세요.

커널 핸들 대기 중

C++/WinRT는 커널 이벤트가 신호를 받을 때까지 일시 중단하는 데 사용할 수 있는 winrt::resume_on_signal 함수를 제공합니다. co_await resume_on_signal(h)가 반환될 때까지 핸들이 유효한 상태로 유지되도록 할 책임이 있습니다. resume_on_signal 자체로는 그 작업을 해줄 수 없습니다. 첫 번째 예제에서처럼 resume_on_signal이 시작되기도 전에 이미 핸들을 잃어버렸을 수도 있기 때문입니다.

IAsyncAction Async(HANDLE event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle is not valid here.
}

들어오는 HANDLE 은 함수가 반환될 때까지만 유효하며, 이 함수(코루틴)는 첫 번째 일시 중단 지점(이 경우 첫 번째 co_await )에서 반환됩니다. DoWorkAsync를 기다리는 동안 컨트롤이 호출자에게 반환되고 호출 프레임이 범위를 벗어났으며 코루틴이 다시 시작될 때 핸들이 유효한지 여부를 더 이상 알 수 없습니다.

기술적으로 코루틴은 값별로 매개 변수를 수신합니다(위의 매개 변수 전달 참조). 그러나 이 경우 우리는 그 안내의 정신을 따르도록 한 걸음 더 나아가야 합니다(편지가 아니라). 핸들과 함께 강력한 참조(즉, 소유권)를 전달해야 합니다. 방법은 다음과 같습니다.

IAsyncAction Async(winrt::handle event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle *is* valid here.
}

winrt::handle을 값으로 전달하면 소유권 의미 체계가 제공되어 커널 핸들이 코루틴의 수명 동안 유효하게 유지됩니다.

코루틴을 호출하는 방법은 다음과 같습니다.

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

이 예제와 같이 시간 제한 값을 resume_on_signal 전달할 수 있습니다.

winrt::handle event = ...

if (co_await winrt::resume_on_signal(event.get(), std::literals::2s))
{
    puts("signaled");
}
else
{
    puts("timed out");
}

비동기 타임아웃을 간편하게

C++/WinRT는 C++ 코루틴에 많은 투자를 합니다. 동시성 코드를 작성하는 방식에 미치는 영향은 혁신적입니다. 이 섹션에서는 비동기의 세부 정보가 중요하지 않은 경우를 설명하며, 원하는 것은 결과뿐입니다. 이러한 이유로 C++/WinRT의 IAsyncAction Windows 런타임 비동기 작업 인터페이스 구현에는 std::future에서 제공하는 것과 유사한 get 함수가 있습니다.

using namespace winrt::Windows::Foundation;
int main()
{
    IAsyncAction async = ...
    async.get();
    puts("Done!");
}

get 함수는 비동기 개체가 완료될 때까지 무기한 블록됩니다. 비동기 개체는 수명이 매우 짧은 경향이 있으므로 필요한 경우가 많습니다.

그러나 충분하지 않은 경우가 있으며 시간이 경과한 후 대기를 포기해야 합니다. Windows 런타임 제공하는 구성 요소 덕분에 항상 코드를 작성할 수 있었습니다. 하지만 이제 C++/WinRT는 wait_for 함수를 제공하여 훨씬 더 쉬워졌습니다. IAsyncAction에서도 구현되며 std::future에서 제공하는 것과 비슷합니다.

using namespace std::chrono_literals;
int main()
{
    IAsyncAction async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        puts("done");
    }
}

메모

wait_for 인터페이스에서 std::chrono::d uration 을 사용하지만 std::chrono::d uration 이 제공하는 것보다 작은 범위(약 49.7일)로 제한됩니다.

다음 예제의 wait_for 약 5초 동안 기다린 다음 완료를 확인합니다. 비교 결과가 기대한 대로라면 비동기 객체가 성공적으로 완료되었다는 것을 알 수 있으며, 이제 끝입니다. 일부 결과를 기다리는 경우 GetResults 메서드를 호출하여 결과를 검색하기만 하면 됩니다.

메모

wait_forget 은 상호 배타적입니다(둘 다 호출할 수 없습니다). 각각 대기자로 간주되며, Windows 런타임 비동기 작업/연산은 하나의 대기자만 지원합니다.

int main()
{
    IAsyncOperation<int> async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        printf("result %d\n", async.GetResults());
    }
}

비동기 개체가 그때까지 완료되었으므로 GetResults 메서드는 더 이상 기다리지 않고 결과를 즉시 반환합니다. 보듯이 wait_for 비동기 개체의 상태를 반환합니다. 따라서 다음과 같이 보다 세분화된 제어에 사용할 수 있습니다.

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;
}
  • AsyncStatus::Completed는 비동기 개체가 성공적으로 완료되었음을 의미하며 GetResults 메서드를 호출하여 결과를 검색할 수 있습니다.
  • AsyncStatus::Canceled 는 비동기 개체가 취소되었음을 의미합니다. 일반적으로 호출자가 취소를 요청하므로 이 상태를 처리하는 경우는 드뭅니다. 일반적으로 취소된 비동기 개체는 단순히 삭제됩니다. 원하는 경우 GetResults 메서드를 호출하여 취소 예외를 다시 throw할 수 있습니다.
  • AsyncStatus::Error 는 비동기 개체가 어떤 식으로든 실패했음을 의미합니다. 원하는 경우 GetResults 메서드를 호출하여 예외를 다시 throw할 수 있습니다.
  • AsyncStatus::Started는 비동기 개체가 계속 실행 중임을 의미합니다. Windows 런타임 비동기 패턴은 여러 대기 또는 웨이터를 허용하지 않습니다. 즉, 루프에서 wait_for 호출할 수 없습니다. 대기가 사실상 시간 초과된 경우, 몇 가지 선택지만 남습니다. 개체를 중단하거나 GetResults 메서드를 호출하여 결과를 검색하기 전에 상태를 폴링할 수 있습니다. 그러나 이 시점에서 개체를 삭제하는 것이 가장 좋습니다.

다른 패턴은 Started에 대해서만 확인하고 GetResults가 다른 경우를 처리하도록 하는 것입니다.

if (async.wait_for(5s) == AsyncStatus::Started)
{
    puts("timed out");
}
else
{
    // will throw appropriate exception if in canceled or error state
    auto results = async.GetResults();
}

배열을 비동기적으로 반환

다음은 오류 MIDL2025 생성하는 MIDL 3.0 의 예입니다. [msg]구문 오류 [context]: 예상 > 또는 "["에 가깝습니다.

Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();

그 이유는 배열을 매개 변수가 있는 인터페이스에 대한 매개 변수 형식 인수로 사용하는 것이 잘못되기 때문입니다. 따라서 런타임 클래스 메서드에서 배열을 비동기적으로 다시 전달하는 목표를 달성하기 위한 덜 명확한 방법이 필요합니다.

배열을 PropertyValue 개체로 박싱하여 반환할 수 있습니다. 그런 다음 호출 코드가 이를 언박싱합니다. 다음은 SampleComponent 런타임 클래스를 Windows 런타임 구성 요소(C++/WinRT) 프로젝트에 추가한 다음, 예를 들어 Blank App, Packaged (WinUI 3 in Desktop) 프로젝트에서 이를 사용해 볼 수 있는 코드 예제입니다.

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

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

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

중요 API