C++/WinRT を使用した高度なコンカレンシーと非同期性

このトピックでは、C++/WinRT でのコンカレンシーと非同期性を備えた高度なシナリオについて説明します。

このテーマの概要については、最初に コンカレンシー操作と非同期操作を参照してください。

Windows スレッド プールへの作業のオフロード

コルーチンは、関数が実行を返すまで呼び出し元がブロックされるという点で、他の関数と同様の関数です。 コルーチンが返される最初の機会は、最初の co_awaitco_return、または co_yieldです。

そのため、コルーチンでコンピューティングバインドされた作業を行う前に、呼び出し元がブロックされないように呼び出し元に実行を返す必要があります (つまり、中断ポイントを導入します)。 他の操作をco_awaitしてまだ実行していない場合は、co_await 関数をできます。 これにより、呼び出し元に制御が返され、スレッド プール スレッドでの実行が直ちに再開されます。

実装で使用しているスレッド プールは低レベルの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.
}

上記のコードは winrt::hresult_wrong_thread 例外をスローします。 これは、TextBlock を作成したスレッド (UI スレッド) から更新する必要があるためです。 解決策の 1 つは、コルーチンが最初に呼び出されたスレッド コンテキストをキャプチャすることです。 これを行うには、winrt::apartment_context オブジェクトをインスタンス化し、バックグラウンド処理を行い、co_awaitして呼び出し元のコンテキストに切り替えます。

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 メソッドを呼び出すことができます)。

ただし、4 つのWindows ランタイム非同期操作の種類 (co_await) のいずれかをした場合、C++/WinRT は、co_awaitした時点で呼び出し元のコンテキストをキャプチャします。 また、継続が再開されたときにも、そのコンテキストのままであることが保証されます。 C++/WinRT は、呼び出し元コンテキストに既に存在するかどうかを確認し、そうでない場合はそれに切り替えることでこれを行います。 co_awaitする前にシングル スレッド アパートメント (STA) スレッドを使用していた場合は、その後同じスレッドになります。co_awaitする前にマルチスレッド アパートメント (MTA) スレッドを使用していた場合は、その後 1 つになります。

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

別の型を co_await 場合は、C++/WinRT コルーチンの実装内であっても、別のライブラリによってアダプターが提供されるため、再開処理やコンテキストに関してそれらのアダプターがどのように動作するかを理解する必要があります。

コンテキストの切り替えを最小限に抑えるために、このトピックで既に説明した手法の一部を使用できます。 それを行ういくつかの図を見てみましょう。 次の擬似コード例では、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 アダプターを記述することから始めます。

Note

以下のコード例は学習目的のみを目的として提供されており、awaitアダプターのしくみを理解し始めるためのものです。 独自のコードベースでこの手法を使用する場合は、独自の await アダプター構造体を開発してテストすることをお勧めします。 たとえば、complete_on_anycomplete_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_readyawait_suspend、およびawait_resumeと呼ばれる関数を検索することを理解する必要があります。 C++/WinRT ライブラリは、このような適切な動作を既定で取得できるように、これらの関数を提供します。

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

no_switch await アダプターを使用するには、次のように、そのco_await式の型を IAsyncXxx から no_switch に変更するだけです。

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

次に、C++ コンパイラは、IAsyncXxx に一致する 3 つのawait_xxx関数を探す代わりに、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 のもう 1 つの一般的な用途は、従来のデスクトップまたは 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);
    }
}

上記の単純な main 関数は、まずウィンドウを作成することから始まります。 これはウィンドウ クラスを登録し、 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;
 
    ...
}

Note

上記のように、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 で 1 秒あたり 1 つのメッセージが 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() メソッドを使用します。

Note

暫定的な結果を報告するには、C++/WinRT バージョン 2.0.210309.3 以降が必要です。

上記の例では、すべての進行状況レポートに一時的な結果を設定することを選択します。 暫定結果を報告するかどうか、また報告する場合はその時期をいつにするかは、自由に選択できます。 進行状況レポートと組み合わせて使用する必要はありません。

Note

非同期アクションまたは非同期操作に対して複数の 完了ハンドラー を実装することは正しくありません。 完了イベントには、デリゲートを 1 つだけ指定することも、co_await することもできます。 両方がある場合、2 つ目は失敗します。 次の 2 種類の完了ハンドラーのいずれかが適切です。同じ非同期オブジェクトの両方ではありません。

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 ランタイム非同期操作の種類の 1 つ、またはコンカレンシー::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.
}

最初の引数 ( 送信者) は名前を付けずに残ります。これは、使用しないためです。 そのため、参照として残しても安全です。 ただし、 引数 が値渡しされていることを確認します。 上記の 「パラメーター渡し」 セクションを参照してください。

カーネル ハンドルを待機しています

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 by value を渡すと所有権セマンティクスが提供されます。これにより、コルーチンの有効期間中、カーネル ハンドルが有効なままになります。

コルーチンを呼び出す方法を次に示します。

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

Note

wait_for はインターフェイスで std::chrono::d uration を使用しますが、 std::chrono::d uration が提供する範囲 (約 49.7 日) よりも小さい範囲に制限されます。

この次の例の wait_for は、約 5 秒間待機し、完了を確認します。 比較が好ましい場合は、非同期オブジェクトが正常に完了したことがわかっており、完了です。 何らかの結果を待っている場合は、 GetResults メソッドを呼び出して結果を取得するだけで、その結果を取得できます。

Note

wait_forget は相互に排他的です (両方を呼び出すことはできません)。 これらはそれぞれ待機者としてカウントされ、非同期アクション/操作Windows ランタイムは 1 つの待機者のみをサポートします。

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 メソッドを呼び出してキャンセル例外を再スローできます。
  • AsyncStatus::Error は、非同期オブジェクトが何らかの方法で失敗したことを意味します。 必要に応じて、 GetResults メソッドを呼び出して例外を再スローできます。
  • 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();
}

配列を非同期的に返す

以下に、error MIDL2025: [msg]syntax error [context]: expecting > or, near "[" が発生する MIDL 3.0 の例を示します。

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

その理由は、パラメーター化されたインターフェイスのパラメーター型引数として配列を使用することは無効であるためです。 そのため、ランタイム クラス メソッドから配列を非同期的に渡すという目的を達成するには、あまり明確ではない方法が必要です。

PropertyValue オブジェクトにボックス化された配列を返すことができます。 その後、呼び出し元のコードによってボックス化が解除されます。 以下にコード例を示します。これは、SampleComponent ランタイム クラスを Windows ランタイム Component (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