次の方法で共有


同時実行ランタイムに関する全般的なベスト プラクティス

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

セクション

このドキュメントは、次のセクションで構成されています。

  • 可能であれば協調的同期コンストラクトを使用する

  • 時間のかかるタスクを譲渡なしで実行するのは避ける

  • オーバーサブスクリプションを使用してブロッキング操作や待機時間の長い操作の影響を軽減する

  • 可能であれば同時実行メモリ管理関数を使用する

  • RAII を使用して同時実行オブジェクトの有効期間を管理する

  • グローバル スコープでは同時実行オブジェクトを作成しない

  • 共有データ セグメントでは同時実行オブジェクトを使用しない

可能であれば協調的同期コンストラクトを使用する

同時実行ランタイムには、外部同期オブジェクトを必要としない同時実行セーフのコンストラクトが多数用意されています。例えば、 concurrency::concurrent_vector クラスが用意されていますと要素の操作へのアクセスの同時実行制御-セーフの追加します。ただし、リソースへの排他アクセスが必要とする場合は、ランタイムを提供、 concurrency::critical_sectionconcurrency::reader_writer_lock、および concurrency::event クラス。これらの型は協調的に動作するため、タスク スケジューラは、最初のタスクがデータを待っている間、処理リソースを別のコンテキストに再割り当てすることができます。可能であれば、協調的に動作しない他の同期機構 (Windows API に用意されている同期機構など) の代わりに、これらの同期型を使用してください。これらの同期型の詳細およびコード例については、「同期データ構造」および「同期データ構造と Windows API の比較」を参照してください。

Top

時間のかかるタスクを譲渡なしで実行するのは避ける

タスク スケジューラは協調的に動作するため、タスク間で公平性は保たれません。したがって、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 つは、長時間実行されるタスクの中でタスク スケジューラに譲渡する時間を不定期に設けることです。次の例では、変更はtask関数を呼び出す、 concurrency::Context::Yield 別のタスクを実行できるようにするには、タスク スケジューラを実行を生成するメソッド。

// 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 オブジェクトしますが、まだ開始されていません。

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

Top

オーバーサブスクリプションを使用してブロッキング操作や待機時間の長い操作の影響を軽減する

同期プリミティブなど、同時実行制御のランタイムを提供 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 関数は潜在的な操作を実行するため、オーバーサブスクリプションを使用することで、現在のタスクがデータを待っている間、他のタスクを実行できるようになります。この例の完全なバージョンについては、「方法: オーバーサブスクリプションを使用して待機時間を短縮する」を参照してください。

Top

可能であれば同時実行メモリ管理関数を使用する

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

これらのメモリ管理関数の詳細については、「タスク スケジューラ (同時実行ランタイム)」を参照してください。これらの関数の使用例については、「方法: Alloc および Free を使用してメモリ パフォーマンスを改善する」を参照してください。

Top

RAII を使用して同時実行オブジェクトの有効期間を管理する

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

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

ランタイムが RAII パターンは、いくつかのクラスを定義する concurrency::critical_section::scoped_lockconcurrency::reader_writer_lock::scoped_lock。これらのヘルパー クラスはスコープ ロックと呼ばれます。作業するときにこれらのクラスはいくつかの利点提供 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 パターンを使用して同時実行オブジェクトの有効期間を管理する方法の別の例については、「チュートリアル: ユーザー インターフェイス スレッドからの処理の除去」、「方法: Context クラスを使用して協調セマフォを実装する」、および「方法: オーバーサブスクリプションを使用して待機時間を短縮する」を参照してください。

Top

グローバル スコープでは同時実行オブジェクトを作成しない

グローバル スコープでは、同時実行のオブジェクトを作成すると、デッドロックやメモリ アクセス違反がアプリケーションで発生することがあります。

など、同時実行制御のランタイム オブジェクトを作成するときは、いずれかまだ作成されていない場合は、ランタイム既定スケジューラを作成します。グローバル オブジェクトの作成中に作成される実行時のオブジェクトは、それに応じてこの既定スケジューラを作成するのには、ランタイムが発生します。ただし、この処理は、内部のロックは、同時実行制御のランタイム インフラストラクチャをサポートするその他のオブジェクトの初期化を妨げる可能性がありますかかります。この内部ロックがまだ初期化されていない、し、アプリケーションでデッドロックが発生する可能性がありますので、別のインフラストラクチャ オブジェクト必要があります。

次の例では、グローバルの作成 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 オブジェクトの正しい作成例については、「タスク スケジューラ (同時実行ランタイム)」を参照してください。

Top

共有データ セグメントでは同時実行オブジェクトを使用しない

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

Top

参照

処理手順

方法: Alloc および Free を使用してメモリ パフォーマンスを改善する

方法: オーバーサブスクリプションを使用して待機時間を短縮する

方法: Context クラスを使用して協調セマフォを実装する

チュートリアル: ユーザー インターフェイス スレッドからの処理の除去

概念

並列パターン ライブラリ (PPL)

非同期エージェント ライブラリ

タスク スケジューラ (同時実行ランタイム)

同期データ構造

同期データ構造と Windows API の比較

並列パターン ライブラリに関するベスト プラクティス

非同期エージェント ライブラリに関するベスト プラクティス

その他の技術情報

同時実行ランタイムに関するベスト プラクティス