Share via


コンカレンシー ランタイムに関する全般的なベスト プラクティス

ここでは、コンカレンシー ランタイムの複数の領域に適用されるベスト プラクティスについて説明します。

セクション

このドキュメントは、次のトピックに分かれています。

可能な場合は協調同期コンストラクトを使用する

コンカレンシー ランタイムには、外部同期オブジェクトを必要としないコンカレンシー セーフのコンストラクトが多数用意されています。 たとえば、concurrency::concurrent_vector クラスを使用すると、コンカレンシー セーフの追加操作と要素アクセス操作を実行できます。 ここでは、コンカレンシー セーフは、ポインターまたは反復子が常に有効であることを意味します。 要素の初期化または特定の走査順序が保証されるわけではありません。 ただし、リソースへの排他アクセスを必要とする場合に備えて、concurrency::critical_section クラス、concurrency::reader_writer_lock クラス、および concurrency::event クラスも用意されています。 これらの型は協調的に動作するため、タスク スケジューラは、最初のタスクがデータを待っている間、処理リソースを別のコンテキストに再割り当てすることができます。 可能であれば、協調的に動作しない他の同期機構 (Windows API に用意されている同期機構など) の代わりに、これらの同期型を使用してください。 これらの同期の種類とコード例の詳細については、「同期データ構造」および「同期データ構造と Windows APIの皮革」を参照してください。

[トップ]

生成されない時間の長いタスクを回避する

タスク スケジューラは協調的に動作するため、タスク間で公平性は保たれません。 したがって、1 つのタスクが原因で他のタスクを開始できなくなることがあります。 このような状況は許容される場合もありますが、デッドロックやスタベーションを引き起こす場合もあります。

次の例では、割り当てられている処理リソースの数を超えるタスクを実行します。 1 つ目のタスクはタスク スケジューラに譲渡しないため、そのタスクが終了するまで 2 つ目のタスクは開始されません。

// cooperative-tasks.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

// Data that the application passes to lightweight tasks.
struct task_data_t
{
   int id;  // a unique task identifier.
   event e; // signals that the task has finished.
};

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

int wmain()
{
   // For illustration, limit the number of concurrent 
   // tasks to one.
   Scheduler::SetDefaultSchedulerPolicy(SchedulerPolicy(2, 
      MinConcurrency, 1, MaxConcurrency, 1));

   // Schedule two tasks.

   task_data_t t1;
   t1.id = 0;
   CurrentScheduler::ScheduleTask(task, &t1);

   task_data_t t2;
   t2.id = 1;
   CurrentScheduler::ScheduleTask(task, &t2);

   // Wait for the tasks to finish.

   t1.e.wait();
   t2.e.wait();
}

この例を実行すると、次の出力が生成されます。

1: 250000000 1: 500000000 1: 750000000 1: 1000000000 2: 250000000 2: 500000000 2: 750000000 2: 1000000000

いくつかの方法を使用して、2 つのタスク間の協調を有効にすることができます。 1 つは、長時間実行されるタスクの中でタスク スケジューラに譲渡する時間を不定期に設けることです。 次の例では、concurrency::Context::Yield メソッドを呼び出すように task 関数を変更し、このメソッドによってタスク スケジューラに実行を譲渡して別のタスクを実行できるようにします。

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();

         // Yield control back to the task scheduler.
         Context::Yield();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

この例を実行すると、次の出力が生成されます。

1: 250000000
2: 250000000
1: 500000000
2: 500000000
1: 750000000
2: 750000000
1: 1000000000
2: 1000000000

Context::Yield メソッドが譲渡するのは、現在のスレッドが属しているスケジューラ上の別のアクティブ スレッド、軽量タスク、または別のオペレーティング システム スレッドのみです。 このメソッドは、concurrency::task_group オブジェクトまたは concurrency::structured_task_group オブジェクトで実行されるようにスケジュールされ、まだ開始されていない処理には譲渡しません。

他の方法を使用して、長時間実行されるタスク間の協調を有効にすることもできます。 大きなタスクを小さなサブタスクに分割できます。 また、時間のかかるタスクの中でオーバーサブスクリプションを有効にすることもできます。 オーバーサブスクリプションを使用すると、使用可能なハードウェア スレッドよりも多くのスレッドを作成できます。 オーバーサブスクリプションは、長時間実行されるタスクの中で非常に長い待機時間 (ディスクやネットワーク接続からのデータの読み取りなど) が発生するような場合に特に役立ちます。 軽量タスクおよびオーバーサブスクリプションの詳細については、「タスク スケジューラ」を参照してください。

[トップ]

オーバーサブスクリプションを使用して、待機時間をブロックまたは長くする操作をオフセットする

コンカレンシー ランタイムには、タスク間での協調的なブロッキングや譲渡を可能にする同期プリミティブ (concurrency::critical_section など) が用意されています。 最初のタスクが協調的なブロッキングや譲渡を行った場合、タスク スケジューラは、そのタスクがデータを待っている間、処理リソースを別のコンテキストに再割り当てすることができます。

コンカレンシー ランタイムに用意されている協調的ブロッキング機構を使用できない場合もあります。 たとえば、使用する外部ライブラリによっては、別の同期機構が使用されることがあります。 別の例としては、Windows API ReadFile 関数を使用してネットワーク接続からデータを読み取る場合など、非常に長い待機時間が発生するような操作を実行する場合です。 このような場合、オーバーサブスクリプションを使用して、該当するタスクがアイドル状態のときに他のタスクが実行されるようにすることができます。 オーバーサブスクリプションを使用すると、使用可能なハードウェア スレッドよりも多くのスレッドを作成できます。

次の関数 download について考えます。この関数では、特定の URL にあるファイルをダウンロードします。 この例では、concurrency::Context::Oversubscribe メソッドを使用して、アクティブ スレッドの数を一時的に増やします。

// Downloads the file at the given URL.
string download(const string& url)
{
   // Enable oversubscription.
   Context::Oversubscribe(true);

   // Download the file.
   string content = GetHttpFile(_session, url.c_str());
   
   // Disable oversubscription.
   Context::Oversubscribe(false);

   return content;
}

GetHttpFile 関数は潜在的な操作を実行するため、オーバーサブスクリプションを使用することで、現在のタスクがデータを待っている間、他のタスクを実行できるようになります。 この例の完全なバージョンについては、「方法: オーバースクリプトを使用して待機時間をオフセットする」を参照してください。

[トップ]

可能な場合は同時実行メモリ管理機能を使用する

有効期間が比較的短い小さなオブジェクトを頻繁に割り当てるような粒度の細かいタスクを実行する場合、メモリ管理関数 concurrency::Alloc および concurrency::Free を使用します。 コンカレンシー ランタイムでは、実行中のスレッドごとに別個のメモリ キャッシュが保持されます。 Alloc 関数と Free 関数は、ロックやメモリ バリアを使用することなく、これらのキャッシュからメモリの割り当てと解放を行います。

これらのメモリ管理関数の詳細については、「タスク スケジューラ」を参照してください。 これらの関数を使用する例については、「方法: Alloc と Free を使用してメモリ パフォーマンスを向上させる」を参照してください。

[トップ]

RAII を使用してコンカレンシー オブジェクトの有効期間を管理する

コンカレンシー ランタイムでは、例外処理を使用して、取り消し処理などの機能を実装します。 したがって、ランタイムを呼び出す場合や、ランタイムを呼び出す別のライブラリを呼び出す場合は、例外セーフなコードを記述してください。

Resource Acquisition Is Initialization (リソース取得は初期化) (RAII) パターンは、特定のスコープでコンカレンシー オブジェクトの有効期間を安全に管理できる方法の 1 つです。 RAII パターンでは、データ構造はスタック上に割り当てられます。 データ構造は、作成されたときにリソースを初期化または取得し、破棄されたときにそのリソースを破棄または解放します。 RAII パターンでは、外側のスコープが終了する前に、常にデストラクターが呼び出されます。 このパターンは、関数に複数の return ステートメントが含まれる場合に便利です。 また、このパターンは、例外セーフなコードを記述するのにも役立ちます。 throw ステートメントによってスタックがアンワインドされると、RAII オブジェクトのデストラクターが呼び出されます。そのため、リソースが常に正しく削除または解放されます。

ランタイムには、concurrency::critical_section::scoped_lock クラスや concurrency::reader_writer_lock::scoped_lock クラスなど、RAII パターンを使用するいくつかのクラスが定義されています。 これらのヘルパー クラスはスコープ ロックと呼ばれます。 これらのクラスは、concurrency::critical_section オブジェクトや concurrency::reader_writer_lock オブジェクトを操作する場合に役立ちます。 これらのクラスのコンストラクターは、提供された critical_section オブジェクトまたは reader_writer_lock オブジェクトへのアクセスを取得し、デストラクターはそのオブジェクトへのアクセスを解放します。 相互排他オブジェクトが破棄されると、スコープ ロックはそのオブジェクトへのアクセスを自動的に解放するため、基になるオブジェクトのロックを手動で解除する必要はありません。

次のクラス account について考えます。このクラスは、外部ライブラリで定義されているため、変更できません。

// account.h
#pragma once
#include <exception>
#include <sstream>

// Represents a bank account.
class account
{
public:
   explicit account(int initial_balance = 0)
      : _balance(initial_balance)
   {
   }

   // Retrieves the current balance.
   int balance() const
   {
      return _balance;
   }

   // Deposits the specified amount into the account.
   int deposit(int amount)
   {
      _balance += amount;
      return _balance;
   }

   // Withdraws the specified amount from the account.
   int withdraw(int amount)
   {
      if (_balance < 0)
      {
         std::stringstream ss;
         ss << "negative balance: " << _balance << std::endl;
         throw std::exception((ss.str().c_str()));
      }

      _balance -= amount;
      return _balance;
   }

private:
   // The current balance.
   int _balance;
};

次の例では、account オブジェクトに対して複数のトランザクションを並列に実行します。 この例では、critical_section オブジェクトを使用して、account オブジェクトへのアクセスを同期します。account クラスはコンカレンシー セーフではないためです。 各並列操作では、critical_section::scoped_lock オブジェクトを使用して、操作が成功または失敗したときに critical_section オブジェクトのロックが確実に解除されるようにします。 口座残高が負の場合、withdraw 操作は例外をスローして失敗します。

// account-transactions.cpp
// compile with: /EHsc
#include "account.h"
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

int wmain()
{
   // Create an account that has an initial balance of 1924.
   account acc(1924);

   // Synchronizes access to the account object because the account class is 
   // not concurrency-safe.
   critical_section cs;

   // Perform multiple transactions on the account in parallel.   
   try
   {
      parallel_invoke(
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before deposit: " << acc.balance() << endl;
            acc.deposit(1000);
            wcout << L"Balance after deposit: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(50);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(3000);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         }
      );
   }
   catch (const exception& e)
   {
      wcout << L"Error details:" << endl << L"\t" << e.what() << endl;
   }
}

この例では、次のサンプル出力が生成されます。

Balance before deposit: 1924
Balance after deposit: 2924
Balance before withdrawal: 2924
Balance after withdrawal: -76
Balance before withdrawal: -76
Error details:
    negative balance: -76

RAII パターンを使用してコンカレンシー オブジェクトの有効期間を管理するその他の例については、「チュートリアル : ユーザーインターフェイス スレッドから作業を削除する」「方法 : コンテキスト クラスを使用して協調セマフォを実装する」、および「方法 : オーバーサブスクリプトを使用して待機時間をオフセットする」を参照してください。

[トップ]

グローバル スコープでコンカレンシー オブジェクトを作成しない

グローバル スコープでコンカレンシー オブジェクトを作成すると、アプリケーションでデッドロックやメモリ アクセス違反などの問題が発生する可能性があります。

たとえば、コンカレンシー ランタイム オブジェクトの作成時にスケジューラがまだ作成されていない場合、既定のスケジューラが作成されます。 グローバル オブジェクトの構築時に作成されるランタイム オブジェクトにより、ランタイムがこの既定のスケジューラを作成します。 ただし、このプロセスでは内部ロックが使用され、コンカレンシー ランタイムのインフラストラクチャをサポートする他のオブジェクトの初期化を妨げる可能性があります。 まだ初期化されていない別のインフラストラクチャ オブジェクトでこの内部ロックが必要になる場合があるため、アプリケーションでデッドロックが発生する可能性があります。

次の例では、グローバルの concurrency::Scheduler オブジェクトを作成する方法を示します。 このパターンは、Scheduler クラスだけでなく、コンカレンシー ランタイムに用意されている他のすべての型にも適用されます。 このパターンはアプリケーションの予期しない動作を引き起こす可能性があるため、使用しないことをお勧めします。

// global-scheduler.cpp
// compile with: /EHsc
#include <concrt.h>

using namespace concurrency;

static_assert(false, "This example illustrates a non-recommended practice.");

// Create a Scheduler object at global scope.
// BUG: This practice is not recommended because it can cause deadlock.
Scheduler* globalScheduler = Scheduler::Create(SchedulerPolicy(2,
   MinConcurrency, 2, MaxConcurrency, 4));

int wmain() 
{   
}

Scheduler オブジェクトの正しい作成例については、「タスク スケジューラ」を参照してください。

[トップ]

共有データ セグメントでコンカレンシー オブジェクトを使用しない

コンカレンシー ランタイムでは、data_seg#pragma ディレクティブによって作成されるデータ セクションなど、共有データ セクションでのコンカレンシー オブジェクトの使用はサポートされていません。 コンカレンシー オブジェクトがプロセス境界をまたいで共有される場合、ランタイムが不整合な状態または無効な状態になる可能性があります。

[トップ]

関連項目

コンカレンシー ランタイムに関するベスト プラクティス
並列パターン ライブラリ (PPL)
非同期エージェント ライブラリ
タスク スケジューラ
同期データ構造
同期データ構造と Windows API の比較
方法: Alloc および Free を使用してメモリ パフォーマンスを改善する
方法: オーバーサブスクリプションを使用して待機時間を短縮する
方法: Context クラスを使用して協調セマフォを実装する
チュートリアル: ユーザー インターフェイス スレッドからの処理の除去
並列パターン ライブラリに関するベスト プラクティス
非同期エージェント ライブラリに関するベスト プラクティス