C++/CX での非同期プログラミング

Note

このトピックは、C++/CX アプリケーションの管理ができるようにすることを目的としています。 ただし、新しいアプリケーションには C++/WinRT を使用することをお勧めします。 C++/WinRT は Windows ランタイム (WinRT) API の標準的な最新の C++17 言語プロジェクションで、ヘッダー ファイル ベースのライブラリとして実装され、最新の Windows API への最上位アクセス権を提供するように設計されています。

この記事では、ppltasks.h の concurrency 名前空間で定義された task クラスを使用して、Visual C++ コンポーネント拡張 (C++/CX) の非同期メソッドを使用する際に推奨される方法について説明します。

Windows ランタイム非同期型

Windows ランタイムには、非同期メソッドを呼び出すためのモデルが明確に定義されており、そのようなメソッドを使う上で必要な型が用意されています。 Windows ランタイムの非同期モデルを初めて使用する場合は、この記事の残りの部分を読む前に「非同期プログラミング」をご覧ください。

非同期 Windows ランタイム API は C++ で直接使用することもできますが、task クラスとそれに関連する型および関数を使用することをお勧めします。これらは concurrency 名前空間に格納されており、<ppltasks.h> 内で定義されています。 concurrency::task は汎用型のクラスですが、/ZW コンパイラ スイッチ (ユニバーサル Windows プラットフォーム (UWP) アプリとそのコンポーネントには必須) を使うと、task クラスで Windows ランタイムの非同期型をカプセル化して次の処理を簡単に行うことができます。

  • 複数の非同期操作および同期操作を連結する

  • タスク チェーンで例外を処理する

  • タスク チェーンで取り消しを実行する

  • 個々のタスクが適切なスレッド コンテキストまたはアパートメントで実行されていることを確認する

ここでは、task クラスを Windows ランタイムの非同期 API で使う方法に関する基本的なガイダンスを示しています。 task およびそれに関連するメソッド (create_task を含む) の詳細については、「タスクの並列化 (コンカレンシー ランタイム)」をご覧ください。

タスクを使用して非同期操作を使用する

次の例は、task クラスを使用して IAsyncOperation インターフェイスを返し、その操作で値を生成する非同期メソッドを使用する方法を示しています。 基本的な手順は次のとおりです。

  1. create_task メソッドを呼び出し、IAsyncOperation^ オブジェクトを渡します。

  2. タスクでメンバー関数 task::then を呼び出し、非同期操作が完了した際に呼び出されるラムダを指定します。

#include <ppltasks.h>
using namespace concurrency;
using namespace Windows::Devices::Enumeration;
...
void App::TestAsync()
{    
    //Call the *Async method that starts the operation.
    IAsyncOperation<DeviceInformationCollection^>^ deviceOp =
        DeviceInformation::FindAllAsync();

    // Explicit construction. (Not recommended)
    // Pass the IAsyncOperation to a task constructor.
    // task<DeviceInformationCollection^> deviceEnumTask(deviceOp);

    // Recommended:
    auto deviceEnumTask = create_task(deviceOp);

    // Call the task's .then member function, and provide
    // the lambda to be invoked when the async operation completes.
    deviceEnumTask.then( [this] (DeviceInformationCollection^ devices )
    {       
        for(int i = 0; i < devices->Size; i++)
        {
            DeviceInformation^ di = devices->GetAt(i);
            // Do something with di...          
        }       
    }); // end lambda
    // Continue doing work or return...
}

task::then 関数によって作成され、返されるタスクは継続と呼ばれます。 ユーザー指定のラムダへの入力引数 (この場合) は、タスク操作が完了した際に生成される結果です。 IAsyncOperation インターフェイスを直接使用している場合、これは IAsyncOperation::GetResults を呼び出すことにより取得されるのと同じ値です。

task::then メソッドはすぐに戻り、そのデリゲートは非同期処理が正常に完了するまで実行されません。 この例では、非同期操作によって例外がスローされた場合、または取り消し要求の結果として取り消された状態で終了した場合、継続が実行されることはありません。 前のタスクが取り消された、または失敗した場合でも、実行される継続を記述する方法については後で説明します。

タスクの変数はローカル スタックで宣言しますが、操作が完了する前にメソッドが返されても、すべての操作が完了し、すべての参照がスコープ外になるまでその有効期間は削除されないように管理されます。

タスクのチェーンを作成する

非同期プログラミングでは、一連の操作 (タスク チェーンとも呼ばれます) を定義するのが一般的です。各継続は、前の継続が完了した場合にのみ実行されます。 場合によっては、継続が入力として受け取る値が前のタスク (または先行タスク) で生成されることもあります。 task::then メソッドを使用すると、簡単かつ直観的な方法でタスク チェーンを作成できます。このメソッドは task<T> を返し、この T はラムダ関数の戻り値の型です。 次のように、1 つのタスク チェーンに複数の継続を作成できます:myTask.then(…).then(…).then(…);

タスク チェーンは、継続によって新しい非同期操作が作成される際に特に役立ちます。このようなタスクは非同期タスクと呼ばれます。 次の例は、2 つの継続があるタスク チェーンを示しています。 最初のタスクは既存のファイルへのハンドルを取得し、その操作が完了すると、1 つ目の継続によって新しい非同期操作が開始され、そのファイルが削除されます。 操作が完了すると、2 つ目の継続が実行され、確認メッセージが出力されます。

#include <ppltasks.h>
using namespace concurrency;
...
void App::DeleteWithTasks(String^ fileName)
{    
    using namespace Windows::Storage;
    StorageFolder^ localFolder = ApplicationData::Current->LocalFolder;
    auto getFileTask = create_task(localFolder->GetFileAsync(fileName));

    getFileTask.then([](StorageFile^ storageFileSample) ->IAsyncAction^ {       
        return storageFileSample->DeleteAsync();
    }).then([](void) {
        OutputDebugString(L"File deleted.");
    });
}

前述の例では、次のような 4 つの重要な点が示されています。

  • 1 つ目の継続では、IAsyncAction^ オブジェクトを task<void> に変換し、 task を返します。

  • 2 つ目の継続では、エラー処理を実行しないため、task<void> ではなく void を入力として受け取ります。 これは値ベースの継続です。

  • 2 番目の継続は、DeleteAsync 操作が完了するまで実行されません。

  • 2 番目の継続は値ベースです。そのため DeleteAsync の呼び出しによって開始された操作が例外をスローした場合、2 番目の継続が実行されることはありません。

注: タスク チェーンの作成は、task クラスを使って非同期操作を構成する方法の 1 つにすぎません。 結合および選択の演算子 (&& および ||) を使用して処理を作成することもできます。 詳細については、「タスクの並列化 (コンカレンシー ランタイム)」を参照してください。

ラムダ関数の戻り値の型とタスクの戻り値の型

タスクの継続では、ラムダ関数の戻り値の型が task オブジェクトでラップされます。 ラムダから double が返される場合、継続タスクの型は task<double> になります。 ただしタスク オブジェクトは、不必要に入れ子になっている戻り値の型を生成しないように設計されています。 ラムダから IAsyncOperation<SyndicationFeed^>^ が返される場合、継続からは、task<task<SyndicationFeed^>> でも task<IAsyncOperation<SyndicationFeed^>^>^ でもなく、task<SyndicationFeed^> が返されます。 このプロセスは非同期のラップ解除と呼ばれます。また、これにより次の継続が呼び出される前に、継続内の非同期操作が確実に完了されます。

前述の例では、ラムダから IAsyncInfo オブジェクトが返されていてもタスクからは task<void> が返されていることに注意してください。 次の表は、ラムダ関数と外側のタスクの間で発生する型変換をまとめたものです。

ラムダの戻り値の型 .then の戻り値の型
TResult task<TResult>
IAsyncOperation<TResult>^ task<TResult>
IAsyncOperationWithProgress<TResult, TProgress>^ task<TResult>
IAsyncAction^ task<void>
IAsyncActionWithProgress<TProgress>^ task<void>
task<TResult> task<TResult>

タスクの取り消し

通常、非同期操作を取り消すオプションをユーザーに提供することをお勧めします。 また場合によっては、タスク チェーンの外部からプログラムで操作を取り消す必要があります。 *Async のそれぞれの戻り値の型には IAsyncInfo から継承した Cancel メソッドが含まれますが、それを外部のメソッドに公開する方法はあまりお勧めできません。 タスク チェーンで取り消しをサポートするときは、cancellation_token_source を使って cancellation_token を作成し、そのトークンを最初のタスクのコンストラクターに渡す方法をお勧めします。 キャンセル トークンを設定して非同期タスクを作成し、[cancellation_token_source::cancel](/cpp/parallel/concrt/reference/cancellation-token-source-class?view=vs-2017& -view=true) が呼び出された場合、タスクは IAsync* 操作で Cancel を自動的に呼び出し、キャンセル要求をその継続チェーンに渡します。 この基本的な方法を示す疑似コードを次に示します。

//Class member:
cancellation_token_source m_fileTaskTokenSource;

// Cancel button event handler:
m_fileTaskTokenSource.cancel();

// task chain
auto getFileTask2 = create_task(documentsFolder->GetFileAsync(fileName),
                                m_fileTaskTokenSource.get_token());
//getFileTask2.then ...

タスクが取り消されると、task_canceled 例外がタスク チェーンを通じて伝達されます。 値ベースの継続は単に実行されませんが、タスクベースの継続では task::get が呼び出されれると例外がスローされます。 エラー処理を行う継続がある場合は、task_canceled 例外を明示的にキャッチするようにしてください。 (この例外は、Platform::Exception から派生したものではありません。)

取り消しは、他の処理と連携して行われます。 継続で UWP メソッドの呼び出しだけではなく時間のかかる作業を行う場合は、キャンセル トークンの状態を定期的に確認し、取り消された場合は実行を中止する必要があります。 継続で割り当てられたすべてのリソースをクリーンアップした後、cancel_current_task を呼び出してそのタスクを取り消し、以降の値ベースの継続に取り消しを伝達します。 また、別の例として FileSavePicker 操作の結果を表すタスク チェーンを作成できます。 ユーザーが [キャンセル] ボタンを選択した場合、IAsyncInfo::Cancel メソッドは呼び出されません。 その代わりに、操作が成功し nullptr が返されます。 この場合、継続で入力パラメーターをテストし、入力が nullptr の場合に cancel_current_task を呼び出すことができます。

詳細については、「PPL における取り消し処理」を参照してください。

タスク チェーンでのエラーの処理

先行タスクの取り消しや例外のスローが行われても継続を実行する場合は、ラムダ関数への入力を task<TResult> または task<void> (先行タスクのラムダから IAsyncAction^ が返される場合) として指定し、継続をタスクベースにします。

タスク チェーンでエラーや取り消しを処理するために、すべての継続をタスクベースにしたり、スローする可能性のあるすべての操作をtry…catch ブロック内に含める必要はありません。 代わりに、チェーンの最後にタスクベースの継続を追加し、そこですべてのエラーを処理できます。 task_canceled 例外はタスク チェーンを通じて伝達され、値ベースの継続はバイパスされるため、エラー処理を行うタスクベースの継続で例外を処理できます。 エラー処理を行うタスクベースの継続を使用するように前述の例を書き換えると次のようになります。

#include <ppltasks.h>
void App::DeleteWithTasksHandleErrors(String^ fileName)
{    
    using namespace Windows::Storage;
    using namespace concurrency;

    StorageFolder^ documentsFolder = KnownFolders::DocumentsLibrary;
    auto getFileTask = create_task(documentsFolder->GetFileAsync(fileName));

    getFileTask.then([](StorageFile^ storageFileSample)
    {       
        return storageFileSample->DeleteAsync();
    })

    .then([](task<void> t)
    {

        try
        {
            t.get();
            // .get() didn' t throw, so we succeeded.
            OutputDebugString(L"File deleted.");
        }
        catch (Platform::COMException^ e)
        {
            //Example output: The system cannot find the specified file.
            OutputDebugString(e->Message->Data());
        }

    });
}

タスクベースの継続では、メンバー関数 task::get を呼び出してタスクの結果を取得します。 task::get はタスクに転送された例外も取得するため、操作が結果を生成しない IAsyncAction であった場合でも、task::get を呼び出す必要があります。 入力タスクが例外を格納している場合は、task::get の呼び出しでそれがスローされます。 task::get を呼び出していない場合、チェーンの最後でタスクベースの継続を使っていない場合、またはスローされた例外の種類をキャッチしていない場合は、タスクに対する参照がすべて削除されたときに unobserved_task_exception がスローされます。

安全に処理できる例外のみをキャッチします。 回復できないエラーがアプリで発生した場合は、不明な状態でアプリの実行を続けるのではなく、アプリをクラッシュさせることをお勧めします。 また、一般に、unobserved_task_exception 自体はキャッチしないことをお勧めします。 この例外は、主に診断を目的としたものです。 unobserved_task_exception がスローされた場合、通常はコード内にバグがあることを示します。 多くの場合、原因は処理が必要な例外か、コード内の他のエラーによって引き起こされる回復できない例外のどちらかです。

スレッド コンテキストを管理する

UWP アプリの UI は、シングル スレッド アパートメント (STA) で実行されます。 ラムダが IAsyncAction または IAsyncOperation を返すタスクは、アパートメント対応です。 タスクが STA で作成されている場合、特に指定しない限り、既定ではその継続もすべて STA で実行されます。 つまり、タスク チェーン全体が親タスクからアパートメントの認識を継承します。 この動作により、STA からのみアクセスできる UI コントロールの操作が簡単になります。

たとえば UWP アプリでは、XAML ページを表す任意のクラスのメンバー関数で、Dispatcher オブジェクトを使用せずに task::then メソッド内から ListBox コントロールを設定できます。

#include <ppltasks.h>
void App::SetFeedText()
{    
    using namespace Windows::Web::Syndication;
    using namespace concurrency;
    String^ url = "http://windowsteamblog.com/windows_phone/b/wmdev/atom.aspx";
    SyndicationClient^ client = ref new SyndicationClient();
    auto feedOp = client->RetrieveFeedAsync(ref new Uri(url));

    create_task(feedOp).then([this]  (SyndicationFeed^ feed)
    {
        m_TextBlock1->Text = feed->Title->Text;
    });
}

タスクが IAsyncAction または IAsyncOperation を返さない場合、そのタスクはアパートメント対応ではなく、既定では、最初に使用可能なバックグラウンド スレッドで継続が実行されます。

既定のスレッド コンテキストは、どちらの種類のタスクについても、task_continuation_context を受け取る task::then のオーバーロードを使用して上書きできます。 たとえば、場合によってはバックグラウンド スレッドでアパートメント対応タスクの継続をスケジュールする方が適している場合もあります。 このような場合は、task_continuation_context::use_arbitrary を渡して、マルチスレッド アパートメント内の次に利用可能なスレッドでタスクの処理をスケジュールできます。 これにより UI スレッドで発生している他の作業と同期する必要がなくなるため、継続のパフォーマンスが向上します。

次の例は、task_continuation_context::use_arbitrary オプションを指定すると役立つ場合が示されています。また、スレッド セーフでないコレクションの同時操作の同期に、既定の継続のコンテキストがどのように役立つのかも示されています。 このコードでは、RSS フィードの URL の一覧をループ処理し、URL ごとに非同期操作を開始してフィード データを取得します。 フィードを取得する順序は制御できませんが、ここでは問題ありません。 各 RetrieveFeedAsync 操作が完了すると、1 つ目の継続が SyndicationFeed^ オブジェクトを受け取り、それを使用してアプリで定義されている FeedData^ オブジェクトを初期化します。 これらの操作はそれぞれ独立した操作であるため、継続のコンテキストとして task_continuation_context::use_arbitrary を指定すると処理が速くなる可能性があります。 ただし、各 FeedData オブジェクトが初期化された後、それらをスレッド セーフなコレクションではない Vector に追加する必要があります。 したがって、継続を作成し、[task_continuation_context::use_current](/cpp/parallel/concrt/reference/task-continuation-context-class?view=vs-2017& -view=true) を指定して、Append のすべての呼び出しが同じアプリケーション シングル スレッド アパートメント (ASTA) コンテキストで確実に実行されるようにします。 task_continuation_context::use_default は既定のコンテキストであるため、明示的に指定する必要はありませんが、ここではわかりやすいように指定しています。

#include <ppltasks.h>
void App::InitDataSource(Vector<Object^>^ feedList, vector<wstring> urls)
{
                using namespace concurrency;
    SyndicationClient^ client = ref new SyndicationClient();

    std::for_each(std::begin(urls), std::end(urls), [=,this] (std::wstring url)
    {
        // Create the async operation. feedOp is an
        // IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^
        // but we don't handle progress in this example.

        auto feedUri = ref new Uri(ref new String(url.c_str()));
        auto feedOp = client->RetrieveFeedAsync(feedUri);

        // Create the task object and pass it the async operation.
        // SyndicationFeed^ is the type of the return value
        // that the feedOp operation will eventually produce.

        // Then, initialize a FeedData object by using the feed info. Each
        // operation is independent and does not have to happen on the
        // UI thread. Therefore, we specify use_arbitrary.
        create_task(feedOp).then([this]  (SyndicationFeed^ feed) -> FeedData^
        {
            return GetFeedData(feed);
        }, task_continuation_context::use_arbitrary())

        // Append the initialized FeedData object to the list
        // that is the data source for the items collection.
        // This all has to happen on the same thread.
        // By using the use_default context, we can append
        // safely to the Vector without taking an explicit lock.
        .then([feedList] (FeedData^ fd)
        {
            feedList->Append(fd);
            OutputDebugString(fd->Title->Data());
        }, task_continuation_context::use_default())

        // The last continuation serves as an error handler. The
        // call to get() will surface any exceptions that were raised
        // at any point in the task chain.
        .then( [this] (task<void> t)
        {
            try
            {
                t.get();
            }
            catch(Platform::InvalidArgumentException^ e)
            {
                //TODO handle error.
                OutputDebugString(e->Message->Data());
            }
        }); //end task chain

    }); //end std::for_each
}

継続内で作成される入れ子になった新しいタスクは、最初のタスクのアパートメントの認識を継承しません。

進行状況の更新を処理する

IAsyncOperationWithProgress または IAsyncActionWithProgress をサポートするメソッドでは、実行中の操作が完了するまでの間、定期的に進行状況の更新が提供されます。 進行状況レポートは、タスクおよび継続の概念とは別で処理されます。 オブジェクト の Progress プロパティのデリゲートのみを指定する必要があります。 デリゲートの一般的な用途は、UI の進行状況バーの更新です。