同時実行ランタイムに関する全般的なベスト プラクティス
ここでは、同時実行ランタイムの複数の領域に適用されるベスト プラクティスについて説明します。
セクション
このドキュメントは、次のセクションで構成されています。
可能であれば協調的同期コンストラクトを使用する
時間のかかるタスクを譲渡なしで実行するのは避ける
オーバーサブスクリプションを使用してブロッキング操作や待機時間の長い操作の影響を軽減する
可能であれば同時実行メモリ管理関数を使用する
RAII を使用して同時実行オブジェクトの有効期間を管理する
グローバル スコープでは同時実行オブジェクトを作成しない
共有データ セグメントでは同時実行オブジェクトを使用しない
可能であれば協調的同期コンストラクトを使用する
同時実行ランタイムには、外部同期オブジェクトを必要としない同時実行セーフのコンストラクトが多数用意されています。 たとえば、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();
}
この例を実行すると、次の出力が生成されます。
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;
}
}
この例では、次のサンプル出力が生成されます。
RAII パターンを使用して同時実行オブジェクトの有効期間を管理する方法の別の例については、「チュートリアル: ユーザー インターフェイス スレッドからの処理の除去」、「方法: Context クラスを使用して協調セマフォを実装する」、および「方法: オーバーサブスクリプションを使用して待機時間を短縮する」を参照してください。
[トップ]
グローバル スコープでは同時実行オブジェクトを作成しない
グローバル スコープで同時実行オブジェクトを作成すると、アプリケーションでデッドロックやメモリ アクセス違反などの問題が発生する可能性があります。
たとえば、同時実行ランタイム オブジェクトの作成時にスケジューラがまだ作成されていない場合、既定のスケジューラが作成されます。 グローバル オブジェクトの構築時に作成されるランタイム オブジェクトにより、ランタイムがこの既定のスケジューラを作成します。 ただし、このプロセスでは内部ロックが使用され、同時実行ランタイムのインフラストラクチャをサポートする他のオブジェクトの初期化を妨げる可能性があります。 まだ初期化されていない別のインフラストラクチャ オブジェクトでこの内部ロックが必要になる場合があるため、アプリケーションでデッドロックが発生する可能性があります。
次の例では、グローバルの 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 ディレクティブによって作成されるデータ セクションなど、共有データ セクションでの同時実行オブジェクトの使用はサポートされていません。 同時実行オブジェクトがプロセス境界をまたいで共有される場合、ランタイムが不整合な状態または無効な状態になる可能性があります。
[トップ]
参照
処理手順
方法: Alloc および Free を使用してメモリ パフォーマンスを改善する
方法: オーバーサブスクリプションを使用して待機時間を短縮する
方法: Context クラスを使用して協調セマフォを実装する
チュートリアル: ユーザー インターフェイス スレッドからの処理の除去