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 ランタイム非同期モデルに慣れていない場合は、この記事の残りの部分を読む前に、「 に関する非同期プログラミング」を参照してください。

C++ で非同期 Windows ランタイム api を直接使用することもできますが、 タスククラスとそれに関連する型および関数を使用することをお勧めします。この方法は、 concurrency名前空間に含まれ、で定義されてい ます。 concurrency:: taskは汎用型ですが、ユニバーサル Windows プラットフォーム (UWP) アプリおよびコンポーネントに必要な/ZWコンパイラスイッチが使用されている場合、タスククラスは Windows ランタイムの非同期型をカプセル化して、次の操作を容易にします。

  • 複数の非同期操作や同期操作を 1 つのチェーンで連結する

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

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

  • 各タスクを適切なスレッド コンテキストまたはスレッド アパートメントで実行する

この記事では、Windows ランタイムの非同期 api でタスククラスを使用する方法についての基本的なガイダンスを提供します。 タスクと、 create_taskを含むその関連メソッドの詳細なドキュメントについては、「 タスクの並列化」 (同時実行ランタイム)を参照してください。

タスクを使った非同期操作の使用

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

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

  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メソッドを使用すると、直感的かつ簡単な方法でタスクチェーンを作成できます。メソッドはタスク 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 つです。

  • 最初の継続は、 iasyncaction ^オブジェクトをタスク void に変換し、タスクを返します。

  • 2番目の継続はエラー処理を実行しません。したがって、タスク voidを入力として取得するのではなく、 voidを受け取ります。 これは値ベースの継続です。

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

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

メモ タスクチェーンの作成は、 タスク クラスを使用して非同期操作を作成する方法の1つにすぎません。 結合演算子 (&&) や選択演算子 (||) を使って操作を構成することもできます。 詳細については、「 タスクの並列化 (同時実行ランタイム)」を参照してください。

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

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

前の例では、ラムダが>オブジェクトを返した場合でもタスクは void > タスクを返すことに注意してください。 ラムダ関数とその外側のタスクの間で行われるこれらの型変換を次の表に示します。

ラムダの戻り値の型 .then の戻り値の型
TResult タスク < TResult>
IAsyncOperation < TResult > ^ タスク < TResult>
IAsyncOperationWithProgress < TResult、TProgress > ^ タスク < TResult>
IAsyncAction^ タスク < void>
IAsyncActionWithProgress < tprogress > ^ タスク < void>
タスク < TResult> タスク < TResult>

タスクの取り消し

通常、非同期操作をユーザーが取り消せるようにすることをお勧めします。 また、場合によっては、プログラムを使ってタスク チェーンの外側から操作を取り消さなければならないこともあります。 各 *非同期戻り値の型にはIasyncinfoから継承するcancelメソッドがありますが、外部のメソッドに公開するのは不便です。 タスクチェーンでの取り消しをサポートするには、 cancellation_token_source を使用して cancellation_tokenを作成し、そのトークンを初期タスクのコンストラクターに渡すことをお勧めします。 キャンセルトークンを使用して非同期タスクが作成され、 cancellation_token_source:: cancelが呼び出された場合、タスクは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 での取り消し」をご覧ください。

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

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

タスク チェーンでエラーや取り消しを処理するために、すべての継続をタスクベースにしたり、スローする可能性があるすべての操作を 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を呼び出してタスクの結果を取得します。 タスク: : 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::thenのオーバーロードを使用task_continuation_context。 たとえば、状況によっては、アパートメントを認識するタスクの継続をバックグラウンド スレッドでスケジュールする方が適している場合もあります。 このような場合は task_continuation_context::use_arbitrary を渡して、マルチスレッド アパートメント内の次に使用可能なスレッドでタスクの作業をスケジュールできます。 これにより、継続の作業を UI スレッドで発生する他の作業と同期する必要がないため、継続のパフォーマンスが向上します。

次の例は task_continuation_context::use_arbitrary オプションを指定すると便利な場合を示しています。また、スレッド セーフでないコレクションに対する同時操作を同期するために既定の継続コンテキストがどのように役立つのかも示しています。 このコードでは、RSS フィードの URL の一覧をループ処理し、各 URL について、非同期操作を開始してフィード データを取得しています。 フィードを取得する順序は制御できませんが、ここでは問題ありません。 RetrieveFeedAsync 操作が完了するたびに、1 つ目の継続が SyndicationFeed^ オブジェクトを受け取り、それを使ってアプリで定義されている オブジェクトを初期化します。 これらの各操作は他の操作とは独立しているので、継続コンテキストに task_continuation_context::use_arbitrary を指定することで、処理を高速化できる可能性があります。 ただし、それぞれの FeedData オブジェクトを初期化した後に、そのオブジェクトを FeedData に追加する必要があり、スレッド セーフなコレクションではありません。 そのため、継続を作成し 、task_continuation_context::use_current を指定して 、Append のすべての呼び出しが同じ Application Single-Threaded Apartment (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 の進行状況バーを更新することです。