C++/WinRT を使用した同時開催操作と非同期操作

重要

このトピックでは、"コルーチン" と co_await の概念について説明します。これは、UI "および" UI のないアプリケーションの両方で使用することをお勧めします。 わかりやすくするために、この入門トピックのコード例のほとんどは、Windows コンソール アプリケーション (C++/WinRT) プロジェクトを示しています。 このトピック内の後述のコード例でコルーチンを使用しますが、便宜上、コンソール アプリケーションの例では、終了する直前にブロッキング get 関数呼び出しも使用して、出力の印刷を完了する前にアプリケーションが終了しないようにしています。 それ (ブロッキング get 関数の呼び出し) は、UI スレッドからは行いません。 代わりに、co_await ステートメントを使用します。 UI アプリケーションで使用する手法の詳細については、「高度な同時実行操作と非同期操作」を参照してください。

この入門トピックでは、C++/WinRT を使用した Windows ランタイムの非同期オブジェクトを作成および利用する方法について少し説明します。 このトピックを読んだ後、特に UI アプリケーションで使用する方法については、「高度な同時実行操作と非同期操作」も参照してください。

非同期操作と Windows ランタイムの "非同期" 関数

完了までの時間が 50 ミリ秒を超える可能性がある Windows ランタイム API は、非同期の関数 (末尾が "Async") として実装されます。 非同期関数を実装すると、別のスレッドの作業が開始され、非同期操作を表すオブジェクトがすぐに返されます。 非同期操作が完了すると、作業結果の値が含まれるオブジェクトが返されます。 Windows::Foundation Windows ランタイムの名前空間には 4 種類の非同期操作オブジェクトが含まれます。

各非同期操作は、winrt::Windows::Foundation C++/WinRT の名前空間で対応する型に投影されます。 また、C++/WinRT には内部 await アダプター構造体も含まれます。 これを直接使用することはありませんが、この構造体により、これらの非同期操作型のいずれかを返す関数の結果を一緒に待機するように co_await ステートメントを記述することができます。 その後、これらの型を返す独自のコルーチンを作成できます。

たとえば、非同期の Windows 関数である SyndicationClient::RetrieveFeedAsync は、IAsyncOperationWithProgress<TResult, TProgress> 型の非同期操作オブジェクトを返します。

それでは、C++/WinRT を使用してそのような API を呼び出すためのいくつかの方法 (最初はブロック、次に非ブロック) を見てみましょう。 基本的なアイデアを示すために、次の複数のコード例では、Windows コンソールアプリケーション (C++/WinRT) プロジェクトを使用します。 UI アプリケーションにより適した手法については、「高度な同時実行操作と非同期操作」を参照してください。

呼び出し元スレッドをブロックする

以下のコード例では、RetrieveFeedAsync から非同期操作オブジェクトを受け取り、非同期操作の結果が利用可能になるまで呼び出し元スレッドをブロックするようにそのオブジェクトで get を呼び出します。

この例を Windows コンソール アプリケーション (C++/WinRT) プロジェクトのメイン ソース コード ファイルに直接コピーして貼り付ける場合は、最初にプロジェクト プロパティで [プリコンパイル済みヘッダーを使用しない] を設定します。

// main.cpp
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void ProcessFeed()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
    // use syndicationFeed.
}

int main()
{
    winrt::init_apartment();
    ProcessFeed();
}

get を呼び出すと、コーディングしやすくなり、何らかの理由でコルーチンを使用しないコンソール アプリやバックグラウンド スレッドに最適です。 しかし、同時実行でも非同期でもないため、UI スレッドには適していません (使用しようとすると、最適化されていないビルドでアサーションが発生します)。 OS スレッドの他の有用な作業を妨げないようにするには、別の方法が必要です。

コルーチンを記述する

C++/WinRT では、C++ コルーチンをプログラミング モデルに統合し、結果を協調的に待機するための自然な方法が提供されます。 コルーチンを記述することで、独自の Windows ランタイム非同期操作を生成することができます。 次のコード例では、ProcessFeedAsync がコルーチンです。

注意

get 関数は、C++/WinRT プロジェクション型 winrt::Windows::Foundation::IAsyncAction に存在するため、任意の C++/WinRT プロジェクト内からこの関数を呼び出すことができます。 get 関数が IAsyncAction インターフェイスのメンバーとして一覧表示されないのは、この関数が実際の Windows ランタイム型 IAsyncAction のアプリケーション バイナリ インターフェイス (ABI) サーフェスの一部ではないからです。

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

IAsyncAction ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    SyndicationFeed syndicationFeed{ co_await syndicationClient.RetrieveFeedAsync(rssFeedUri) };
    PrintFeed(syndicationFeed);
}

int main()
{
    winrt::init_apartment();

    auto processOp{ ProcessFeedAsync() };
    // do other work while the feed is being printed.
    processOp.get(); // no more work to do; call get() so that we see the printout before the application exits.
}

コルーチンは中断して再開できる関数です。 上記の ProcessFeedAsync コルーチンでは、co_await ステートメントに到達すると、コルーチンは RetrieveFeedAsync 呼び出しを非同期的に開始してからすぐに自身を中断し、呼び出し元 (上記の例では main) に制御を戻します。 その後、フィードが取得および印刷されている間、main では作業を続けることができます。 完了すると (RetrieveFeedAsync 呼び出しが完了すると)、ProcessFeedAsync コルーチンは次のステートメントを再開します。

コルーチンを他のコルーチンに集約することができます。 または、get を呼び出してブロックするか、完了するまで待機できます (存在する場合は結果を取得します)。 または、Windows ランタイムをサポートする別のプログラミング言語に渡すことができます。

また、デリゲートを使用して、非同期アクションおよび操作の完了と進行状況イベントを処理することもできます。 詳細とコード例については、「非同期アクションと操作のデリゲート型」をご覧ください。

見てわかるように、上記のコード例では、引き続き、main の終了直前にブロッキング get 関数呼び出しを使用しています。 これは、出力の印刷が完了する前にアプリケーションが終了しないようにすることのみが目的です。

Windows ランタイム型を非同期的に返す

この次の例では、特定の URI に対して RetrieveFeedAsync の呼び出しをラップして、SyndicationFeed を非同期的に返すRetrieveBlogFeedAsync 関数を示します。

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

IAsyncOperationWithProgress<SyndicationFeed, RetrievalProgress> RetrieveBlogFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    return syndicationClient.RetrieveFeedAsync(rssFeedUri);
}

int main()
{
    winrt::init_apartment();

    auto feedOp{ RetrieveBlogFeedAsync() };
    // do other work.
    PrintFeed(feedOp.get());
}

以下の例では、RetrieveBlogFeedAsync は、進捗状況と戻り値の両方が含まれる IAsyncOperationWithProgress を返します。 RetrieveBlogFeedAsync が自分の処理を行い、フィードを取得している間は、他の作業を行うことができます。 次に、非同期操作オブジェクトで get を呼び出し、ブロックするか、完了するまで待ってから、操作結果を取得します。

Windows ランタイム型を非同期的に返す場合は、IAsyncOperation<TResult> または IAsyncOperationWithProgress<TResult, TProgress> を返す必要があります。 ファースト パーティまたはサード パーティのランタイム クラス、または Windows ランタイム関数との間で受け渡しできる任意の型 (たとえば、int または winrt::hstring) がこれに適合します。 Windows ランタイム型以外でこれらの非同期操作型のいずれかを使用しようとすると、コンパイラからのサポートとして "T は WinRT 型である必要があります" というエラーが表示されます。

コルーチンに 1 つ以上の co_await ステートメントがない場合、コルーチンであると認められるために、1 つ以上の co_return または 1 つの co_yield ステートメントが必要です。 非同期操作を必要とせず、そのためにコンテキストのブロックや切り替えを行わずに、コルーチンが値を返すことができる場合があります。 値をキャッシュすることでそれを行う例を次に示します (2 回目以降は呼び出されます)。

winrt::hstring m_cache;

IAsyncOperation<winrt::hstring> ReadAsync()
{
    if (m_cache.empty())
    {
        // Asynchronously download and cache the string.
    }
    co_return m_cache;
}

Windows ランタイム型以外を非同期的に返す

Windows ランタイム型以外の型を非同期的に返す場合は、並列パターン ライブラリ (PPL) concurrency::task を返す必要があります。 std::future よりもパフォーマンスに優れているため (また、今後の互換性の向上により)、concurrency::task をお勧めします。

ヒント

<pplawait.h> を含めると、コルーチンの型として concurrency::task を使用できます。

// main.cpp
#include <iostream>
#include <ppltasks.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

concurrency::task<std::wstring> RetrieveFirstTitleAsync()
{
    return concurrency::create_task([]
        {
            Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
            SyndicationClient syndicationClient;
            SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
            return std::wstring{ syndicationFeed.Items().GetAt(0).Title().Text() };
        });
}

int main()
{
    winrt::init_apartment();

    auto firstTitleOp{ RetrieveFirstTitleAsync() };
    // Do other work here.
    std::wcout << firstTitleOp.get() << std::endl;
}

パラメーターの引き渡し

同期関数では、既定で const& パラメーターを使用する必要があります。 これにより、コピーのオーバーヘッドが回避されます (参照カウント、つまり、インターロックされたインクリメントとデクリメントが含まれます)。

// Synchronous function.
void DoWork(Param const& value);

ただし、コルーチンに参照パラメーターを渡した場合、問題が発生する可能性があります。

// NOT the recommended way to pass a value to a coroutine!
IASyncAction DoWorkAsync(Param const& value)
{
    // While it's ok to access value here...

    co_await DoOtherWorkAsync(); // (this is the first suspension point)...

    // ...accessing value here carries no guarantees of safety.
}

コルーチンでは、最初の一時停止ポイントまでは実行は同期であり、そこで呼び出し元に制御が返され、呼び出しフレームが範囲外になります。 コルーチンが再開するまでに、参照パラメーターが参照するソース値に何かが起こった可能性があります。 コルーチンの観点から、参照パラメーターの有効期間は制御されません。 そのため、上記の例では、co_await までに安全にアクセスできますが、それ以降は安全ではありません。 呼び出し元によって "" が破棄された場合、その後にコルーチン内で値にアクセスしようとすると、メモリが破損する結果になります。 また、その後その関数が中断し、再開した後でを使用しようとするリスクがある場合、DoOtherWorkAsync に安全にを渡すこともできません。

中断して再開した後で、パラメーターを安全に使用できるようにするには、コルーチンでは値渡しを既定で使用し、値によってキャプチャされて有効期間の問題が回避されるようにする必要があります。 それを行うことが安全であると確信しているため、そのガイダンスから逸脱できる場合はまれです。

// Coroutine
IASyncAction DoWorkAsync(Param value); // not const&

値で渡すには、引数の移動またはコピーが低コストである必要があり、通常はスマート ポインターを使用します。

(値を移動しない限り) 定数値を渡すことが良い方法であるということにも議論の余地があります。 コピー元のソース値に影響はありませんが、意図が明確になり、誤ってコピーを変更した場合に役立ちます。

// coroutine with strictly unnecessary const (but arguably good practice).
IASyncAction DoWorkAsync(Param const value);

非同期の呼び出し先に標準ベクターを渡す方法について説明した「標準的な配列とベクトル」も参照してください。

コルーチンのシグネチャを変更することはできなくても、実装は変更できる場合は、最初の co_await の前にローカル コピーを作成することができます。

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_value = value;
    // It's ok to access both safe_value and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_value here (not value).
}

Param のコピーが高コストの場合は、最初の co_await の前に、必要な部分だけを抽出します。

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_data = value.data;
    // It's ok to access safe_data, value.data, and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_data here (not value.data, nor value).
}

class-member コルーチンで this ポインターに安全にアクセスする

C++/WinRT の強参照と弱参照」をご覧ください。

重要な API