同時実行の問題

食事する哲学者の問題を Asynchronous Agents で解決する

Rick Molloy

この記事は、Visual C++ 2010 のプレリリース版に基づいています。記載されている内容は変更されることがあります。

コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

目次

食事する哲学者
アクタベースのアプローチ
5 つのクラスのソリューション
メッセージ ブロックとメッセージ
エージェントと join メッセージ ブロック
Philosopher をテストし、状態を表示する
Table クラスを実装する
ランチの時間

Visual Studio 2010 の重要なテーマは、C++ 開発者が同時処理能力の高いアプリケーションを記述できるようにすることです。ベータ リリースには Concurrency Runtime、並列ライブラリ、および開発ツールが含まれています。これらを使用して、いくつかの一般的な問題に対処し、マルチコア ハードウェアに備わっているパフォーマンスの潜在能力を開発者が活用できるようにします。特に、開発者がコードで同時実行の機会を識別してそれを活用し、共有状態とその副産物を生産的に管理できるようにすると共に、オーバーヘッドの小さい (さまざまなハードウェアで実行時に拡張可能な) 同時実行インフラストラクチャの構築について開発者が心配せずに済むようにします。

この記事では、Visual C++ 2010 の一部として含まれている新しい Asynchronous Agents Library を使用して、共有状態で発生する可能性のある困難な問題の対処方法について説明します。そのしくみを示すために、同時実行の古典的な問題を実装する手順を詳しく見ていきましょう。古典的な問題とは、ダイクストラ氏が提示した "食事する哲学者の問題" です。スレッド プリミティブまたは同期プリミティブに直接依存しない、適正で理解しやすいソリューションを提供して問題を解決するために、エージェントのアクタベース プログラミング コンストラクトと非同期メッセージ渡し API との組み合わせをどのように利用するのか、この記事を通して皆さんにおわかりいただけると思います。

食事する哲学者

あまり詳しくないという方のためにご説明しますが、"食事する哲学者の問題" はマルチスレッド環境で共有状態を管理する複雑さを示しています。それは下記のような問題です。

5 人の哲学者が円形のテーブルを囲んでいます。それぞれが、思索と食事 (ご飯の入った 1 つの大きな容器から食べる) を任意の間隔で交互に行います。哲学者たちにとって都合の悪いことに、テーブルには箸が 5 本しかありません (図 1 を参照)。もちろん、食事には 2 本の箸が必要です。箸は哲学者たちの間で共有されるため、箸へのアクセスには保護が必要ですが、注意を怠ると同時実行の問題が発生します。最も大きな問題は、デッドロック状態で動きがとれなくなり、次のようなプロセスで (比喩表現としての) 飢餓が発生することです。哲学者がそれぞれ空いた箸を 1 本ずつ取り、もう 1 本の箸を手にするチャンスをひたすら待つとすれば、最終的にだれも 1 組の箸を確保できません。

fig01.gif

図 1 哲学者たちと箸

食事する哲学者の問題をコードでの表現に置き換えると、哲学者がスレッドで、箸はある種の共有状態を示します。この問題の簡単な解決法でしばしば使用されるのは、給仕エンティティを導入して箸へのアクセスを調整する、ロック順序付けヒューリスティックを導入する、およびスレッド API、クリティカル セクション、セマフォ、またはモニタを手動で操作して状態を管理することなどです。

大半の開発者は賛同してくれると思いますが、複雑なロックを適正に構築するということは、どう考えても厄介で失敗しやすいと見なされています。そのため、食事する哲学者の問題のほとんどの実装で、実装の詳細が哲学者にリークされる傾向があります。たとえば、Philosopher が同期プリミティブ (または他の Philosopher の箸) を認識できるかもしれません。これが特に難題となるのは、問題を任意の数の哲学者で汎用化する場合、つまり、同期プリミティブやスレッド プリミティブの代わりに、問題領域 (たとえば、それぞれの哲学者が何をするか) の実装に焦点を移した場合です。

アクタベースのアプローチ

Asynchronous Agents Library に基づいて構築される設計について見ていきましょう。食事する哲学者の問題には、やや難しい部分が 2 つだけあります。各哲学者が実行するスレッドの作成と、哲学者による箸へのアクセスの調整です。Asynchronous Agents Library では、アクタベースのプログラミング モデルおよび非同期メッセージ渡し API が提供されます。この記事の実装では両方とも必要です。

Asynchronous Agents Library では、エージェント クラスによりアクタ パターンがモデル化され、非同期実行メソッドが提供されます。私は、各哲学者にエージェントを使用し、実行メソッドの利点を活用して、各自が固有のスレッドで動作する各哲学者の要件を満たすことにします。

適切なソリューションの 2 番目の部分は、箸への同時実行アクセスの管理が関連しています。セマフォまたはロックを使用する従来のソリューションとは対照的に、私は Asynchronous Agents Library のメッセージ渡し API を使用して、哲学者の手とテーブルの間で箸を動かします。

各哲学者が思索と食事の間を移行するときに、"箸を取って、テーブルに返す" という行動はメッセージの送信により行われます。おわかりのとおり、これによりスレッドおよび共有状態に何らかの抽象要素を提供するだけでなく、他の有効なソリューションよりも問題の領域にさらに焦点を当てることができます。

5 つのクラスのソリューション

基本的な設計に 5 つの型を使用します。

  • 比較的わかりやすい Chopstick クラス。
  • 箸をテーブルの上に保持して哲学者に提供するために使用する ChopstickProvider クラス。
  • 思索と食事を担当し、それら 2 つの ChopstickProvider のみを認識する Philosopher クラス。
  • 思索または食事を実行できる PhilospherState 列挙型。
  • Philosopher のセットと Chopstick のセットを含む Table クラス。Table は、1 つの Chopstick を ChopstickProvider と各 Philosopher の間に配置することにより、テーブルを設定する役割を負っています。図 2 は、クラスの関係を示しています。

fig02.gif

図 2 食事する哲学者のソリューションで使用されるクラス

まず、最も簡単なクラスから説明しましょう。Chopstick。Chopstick の目的は、単純に箸として存在し、それ自体を識別できるようにすることです。したがって、その実装は、識別子メンバ変数、識別子を初期化するコンストラクタ、および識別子を返すメソッドを持つ単純なクラスです。

class Chopstick{
  const std::string m_Id;
public:
  Chopstick(std::string && Id):m_Id(Id){};
  const std::string GetID() {
    return m_Id;
  };
};

特に、識別子フィールドの m_Id が const メンバ変数である点に注目してください。他のスレッドが意図せずに変更する可能性のある状態は、箸が持つ状態として好ましくありません。さらに、コンストラクタがパラメータ リスト (&&) で r 値参照を使用していることにも注目してください。これは、C++0x 標準の草案の一部であり、Visual Studio 2010 の新しい言語コンストラクトです。ここでの r 値参照により、コンパイラは、Id が一時的な変数または r 値である場合、copy コンストラクタ呼び出しを回避して、Id を Chopstick インスタンス内の m_Id に移動することができます。

メッセージ ブロックとメッセージ

ChopstickProvider も簡単です。これは、Chopstick が送信されたときに受け入れ、要求に応じて Chopstick を Philosopher に提供することを目的とした、ストレージ バッファと考えることができます。

これから説明するのは、エージェント API を使用する最初の場面です。ChopstickProvider に対して、メッセージ ブロックとして知られているものを使用します。メッセージ ブロックは、メッセージの送受信を行うことができるクラスとして定義されます。具体的には、メッセージをメッセージ ブロックに送信できる場合、そのブロックはターゲットと見なされ、メッセージ ブロックからメッセージを受信できる場合はソースと見なされます。

この例では、ChopstickProvider がソースとターゲットの両方の役割を担う必要があります。Philosopher は空腹なときに ChopstickProvider から箸を受け取り、Philosopher が再び思索する準備ができたときに箸を ChopstickProvider に送り返します。次に、これらの動作を実行する例を示します。

//create a chopstick
Chopstick chopstick("chopstick one");

//create a ChopstickProvider to store the chopstick
ChopstickProvider chopstickBuffer;

//put the chopstick in the chopstick holder
Concurrency::send(chopstickBuffer, &chopstick);

//request a chopstick from the chopstick buffer
Chopstick* result = Concurrency::receive(chopstickBuffer);

おわかりだと思いますが、箸および chopstickBuffer (まだ定義されていない ChopstickProvider のインスタンス) を宣言した後で、Concurrency::send を使用して箸のアドレスを ChopstickBuffer に配置します。Concurrency::receive が、chopstickBuffer から Chopstick へのポインタを返します。Concurrency::send は、メッセージをメッセージ ブロックに同期的に配置するテンプレート メソッドです。Concurrency::receive は、メッセージをメッセージ ブロックから返すテンプレート メソッドです。Concurrency::receive は、メッセージがメッセージ ブロックで利用可能になるまで、ブロックします。

Asynchronous Agents Library では、Concurrency::asend と Concurrency::try_receive も提供されます。Concurrency::asend は、受信の通知を待たずにメッセージを送信するタスクを非同期的に発生させます。Concurrency::try_receive は、メッセージを取得する (利用可能な場合) 非ブロック呼び出しです。

ここで、実際に ChopstickProvider を定義しましょう。簡単だと言いましたよね。typedef を使用します。

typedef Concurrency::unbounded_buffer<Chopstick*> 
  ChopstickProvider;

エージェント ライブラリでは、必要とされる機能を備えた unbounded_buffer というメッセージ ブロックが提供されます。unbounded_buffer は、ソースとターゲットの両方になるテンプレート クラスであり、メモリ制限に基づいた数のメッセージを内部キューに格納します。メッセージは、要求されたときにキューから削除されます。この "食事する哲学者の問題" の実装では、無制限の特性は必要はありません。箸を保持する者の間で移動する箸の数が限られているからです。unbounded_buffer で主に使用する機能は、移動セマンティクスです。

エージェントと join メッセージ ブロック

では、Philosopher クラスの実装を見ていきましょう。これは、食事する哲学者の問題の興味深い部分です。それぞれの哲学者が固有のスレッドで動作し、共有リソースへのアクセスが適切に調整されるようにします。Philosopher クラスは、Asynchronous Agents Library のコンストラクトを 2 つ追加して使用します。抽象エージェント クラスと join コンストラクトです。最初に、エージェントについて説明し、使用方法を示します。

エージェント クラスは、アクタ パターンまたはアクタベースのプログラミングとも呼ばれるものをモデル化する抽象クラスです。エージェントを使用する目的は、エージェントで状態をカプセル化することによってアプリケーションのアクティブなコンポーネント間の依存関係および共有状態をスムーズに排除できるようにすること、およびパブリック インターフェイスの小さなセットを通過するメッセージを使って相互にやり取りすることです。これは、一見非常に複雑に見えますが、さいわい Philosopher クラスと似ている点があります。

前述したとおり、Philosopher クラスはそのクラスに固有のスレッドで実行する必要があります。これをエージェントの言語で言い換えると、アクティブなタスクにする必要があるということです。したがって、Philosopher は Concurrency::agent からパブリックに派生します。また、Philosopher クラスは、2 つの ChopstickProvider (両手に 1 つずつ) を認識し、それらを使用して思索と食事の間を適切に移行する必要があります。Philosopher クラスはまだ完成していませんが、これがどのようにクラスのシノプシスへと変わっていくのか、皆さんはおわかりだと思います。

enum PhilosopherState {Thinking,Eating};

typedef Concurrency::unbounded_buffer<Chopstick*> ChopstickProvider;
class Philosopher : public Concurrency::agent {
  ChopstickProvider* m_LeftChopstickProvider;
  ChopstickProvider* m_RightChopstickProvider;

public:
  const std::string m_Name;
  const int  m_Bites;
  Philosopher(const std::string&& name, 
    int bites=500):m_Name(name),
    m_Bites(bites){};
...
}

Philosopher が持っているのは、2 つの ChopstickProvider へのポインタ、名前、Philosopher が取得するバイト数 (Philosopher が交互に行う "思索" と "食事" の切り替え回数)、および初期化のためのコンストラクタです。

ここに含まれていないものは、エージェントで ChopstickProvider を初期化する方法です。Philosopher は Chopstick およびその ChopstickProvider に依存しないので、コンストラクト時に ChopstickProvider を初期化する必要はないと思います。AssignChopsticks (ChopstickProvider* left, ChopstickProvider* right) のような追加のパブリック メソッドを作成することもあるでしょう。ただし、その場合は、このメソッドが確実にスレッド セーフになるように何らかの対応を行う (ロックする) ことが必要です。

その代わりに、私は、ChopstickProvider の割り当てを非同期で行うためのパブリック インターフェイスを作成します。まず、2 つのパブリックな unbounded_buffer メンバ変数を追加し、これらを使用して Philosopher に ChopstickProvider を送信します。

Concurrency::unbounded_buffer<ChopstickProvider*> 
  LeftChopstickProviderBuffer;
Concurrency::unbounded_buffer<ChopstickProvider*> 
  RightChopstickProviderBuffer;

これで、Philosopher メソッドの実装を開始する準備が整いました。このメソッドは、固有のスレッドで動作し、ChopstickProvider が初期化されるまで待機し、続いて思索と食事の切り替えを開始します。私は、エージェント基本クラスから仮想の実行メソッドを実装します。agent::run は、agent::start が呼び出されたときに固有のタスクで開始されます。

void run() {

内部実行のために最初に行う必要があることは、パブリック メソッドで receive を使用してメッセージの送信を待機することにより、ChopstickProvider を初期化することです。

//initialize the ChopstickProviders
m_LeftChopstickProvider  = 
  Concurrency::receive(LeftChopstickProviderBuffer);
m_RightChopstickProvider = 
  Concurrency::receive(RightChopstickProviderBuffer);

次に、思索と食事の間で切り替えを行う必要があります。そのために、2 つの追加のメソッドを使用します。PickupChopsticks と PutDownChopsticks です。

for(int i = 0; i < m_Bites;++i) {
  Think();
  std::vector<Chopstick*> chopsticks(PickupChopsticks());
  Eat();
  PutDownChopsticks(chopsticks);
}

実行メソッドで実施する最後の手順は、必要なときに再利用できるように ChopstickProvider を返してクリーン アップすること、およびエージェント ステータスを done に設定することです。

Concurrency::send(LeftChopstickProviderBuffer,  
  m_LeftChopstickProvider);
Concurrency::send(RightChopstickProviderBuffer, 
  m_RightChopstickProvider);

this->done(Concurrency::agent_done);
}

次に進みましょう。追加のメソッドの Eat と Think は簡単です。元の問題の説明では、これらを任意のスピン ループとして見なしています。

private:
  void Eat() {
    RandomSpin();
  };

  void Think() {
    RandomSpin();
  };
};

PickupChopsticks の実装は、非常に興味深いものです。この実装により、箸の取得と開放をデッドロックまたは競合なしで行うことができるからです。この実装のために、join という Asynchronous Agents Library の別のメッセージ ブロックを使用します。Concurrency::join は、複数のソースからのメッセージ (潜在的にさまざまな型) を待機するメッセージ ブロックです。すべてのメッセージを受信すると、複合的なメッセージが生成されます。join では、同じ型または複数の型のソースをサポートすることができ、貪欲法または非貪欲法でメッセージを取得します。つまり、エージェント ライブラリには join の 4 つのバリアントがあります。Philosopher クラスは、1 つの型の非貪欲法の join を使用します。図 3 は、join の単純な例をコードで示しています。

図 3 単純な join

//create two chopsticks
Chopstick chopstick1("chopstick one");
Chopstick chopstick2("chopstick two");

//create ChopstickProviders to store the chopstick
ChopstickProvider chopstickBuffer1;
ChopstickProvider chopstickBuffer2;

//put a chopstick in each chopstick holder
Concurrency::send(chopstickBuffer1, &chopstick1);
Concurrency::send(chopstickBuffer2, &chopstick2);

//declare a single-type non greedy join to acquire them.
//the constructor parameter is the number of inputs
Concurrency::join<Chopstick*,non_greedy> j(2);

//connect the chopstick providers to the join so that messages 
//sent will propagate forwards
chopstickBuffer1.link_target(&j);
chopstickBuffer2.link_target(&j);

//the single type join message block produces a vector of Chopsticks
std::vector<Chopstick*> result = Concurrency::receive(j);

これで、PickupChopsticks を実装する準備は完了です。とりわけ目を引く部分は、ベクタを返すことによって箸のセットを返すことでしょう。箸の提供元から 1 組の箸を取得することは、非貪欲法による join を使用することを意味します。非貪欲法による join のバリアントは、すべてのリンクされたソースからメッセージが提供されるまで待機します。次に、各ソースからメッセージが提供されると、実際にメッセージの所有権を取得できることを確認します。この例では、これがデッドロックを回避する方法です。join のテンプレート パラメータとして greedy を使用した場合、メッセージは提供されるとすぐに取得され、結局は各 Philosopher が箸を 1 本ずつ入手し、全体がデッドロック状態になってしまうでしょう。

次に、PickupChopsticks のコードを示します。join を作成し、箸の提供者にリンクし、箸が到着するのを待ちます。

std::vector<Chopstick*> PickupChopsticks() {
  //create the join
  Concurrency::join<Chopstick*,Concurrency::non_greedy> j(2);
  m_LeftChopstickProvider->link_target(&j); 
  m_RightChopstickProvider->link_target(&j);

  //pickup the chopsticks
  return Concurrency::receive (j);
}

PutDownChopsticks の実装も簡単です。必要なのは、非同期メソッドの asend を使用してベクタから入手した箸を返すことだけです。

void PutDownChopsticks(std::vector<Chopstick*>& v) {
  Concurrency::asend(m_LeftChopstickProvider,v[0]);
  Concurrency::asend(m_RightChopstickProvider,v[1]);
};

Philosopher をテストし、状態を表示する

Philosopher クラスはそのまま使用できますが、Philosopher クラスを開始する方法を理解し、ステータス (食事または思索のどちらか) を報告する方法を用意する必要があります。

Philosopher を開始するには、仮想実行メソッドを起動するタスクを生成する、agent::start メソッドを呼び出します。ステータスを報告するために、エージェント ライブラリからメッセージ ブロックを 2 つ追加します。Concurrency::overwrite_buffer と Concurrency::call です。

Concurrency::overwite_buffer<T> は、上書き可能な 1 つの値を格納するメッセージ ブロックであり、要求に応じて値のコピーを生成します。他のメッセージ ブロック同様、overwrite_buffer を追加のターゲットにリンクすることができ、メッセージの伝達が順番に行われます。パブリック メンバ変数 (overwrite_buffer<PhilosopherState>) を Philosopher クラスに追加し、Philosopher が Thinking から Eating に移行するときにメッセージを送信することによってそのメンバ変数を更新します。具体的には、これには、Philosopher クラスへのメンバ変数の追加が含まれます。

Concurrency::overwrite_buffer<PhilosopherState> CurrentState;

また、Eat および Think を変更してステータスを更新することも意味します。

void Eat() {
  send(&CurrentState,PhilosopherState::Eating);
  RandomSpin();
};

void Think() {
  send(&CurrentState,PhilosopherState::Thinking);
  RandomSpin();
};

Concurrency::call<T> は、関数記号を使用して構築されるメッセージ ブロックであり、メッセージを受信したときに関数記号を実行するタスクを生成します。図 4 に示すように、Philosopher でこの呼び出しと新しく定義した overwrite_buffer とを組み合わせて使用し、状態を報告することができます。

図 4 Philosopher の状態を報告する

//create an instance of a Philosopher and have it take 5 bites
Philosopher Descartes("Descartres",5);

//create a pair of chopsticks 
Chopstick chopstick1("chopstick1");
Chopstick chopstick2("chopstick2");

//create a pair of ChopstickProviders
ChopstickProvider chopstickBuffer1;
ChopstickProvider chopstickBuffer2;

//associate our chopstick providers with Descartes
Concurrency::send(&Descartes.LeftChopstickProvider, chopstickBuffer1);
Concurrency::send(&Descartes.RightChopstickProvider, chopstickBuffer2);

//start the Philosopher asynchronously 
agent::start(&Descartes);

//declare a call that will be used to display the philosophers state
//note this is using a C++0x lambda
Concurrency::call<PhilosopherState> display([](PhilosopherState state){
  if (state ==  Eating)
    std::cout << "eating\n" ;
  else
    std::cout << "thinking\n" ;
});

//link the call to the status buffer on our Philosopher
Descartes.CurrentState .link_target (&display);

//start our Philosopher eating / thinking by sending chopsticks
asend(&chopstickBuffer1,&chopstick1);
asend(&chopstickBuffer2,&chopstick2);

Philosopher クラスに関しては以上です。

Table クラスを実装する

Table クラスを作成するために必要なものはすべてご覧になりましたね。前述のとおり、Philosopher のセット、Chopstick のセット、および ChopstickProvider のセットが含まれます。Table の役割は、Philosopher をテーブルに着かせ、ChopstickProvider を間に配置し、Chopstick をそれぞれの ChopstickProvider に配置することです。柔軟性を高めるために、私はこれをテンプレート クラスにすることにしました。このクラスには、Philosopher のリストへの参照、Chopstick のベクタ、および Chopstick Holder のベクタが含まれます。

template<class PhilosopherList>
class Table {
  PhilosopherList & m_Philosophers;
  std::vector<ChopstickProvider*> m_ChopstickProviders;
  std::vector<Chopstick*> m_Chopsticks;
  ...

Table クラスの唯一のパブリック クラスはコンストラクタです。コンストラクタは、ベクタを初期化し、ChopstickProvider を各 Philosopher に割り当てます (図 5 を参照)。

図 5 Table コンストラクタ

Table(PhilosopherList& philosophers): m_Philosophers(philosophers) {
  //fill the chopstick and the chopstick holder vectors
  for(size_t i = 0; i < m_Philosophers.size();++i) {
    m_ChopstickProviders.push_back(new ChopstickProvider());
    m_Chopsticks.push_back(new Chopstick("chopstick"));

    //put the chopsticks in the chopstick providers
    send(m_ChopstickProviders[i],m_Chopsticks[i]);
  }

  //assign the philosophers to their spots
  for(size_t leftIndex = 0; 
    leftIndex < m_Philosophers.size();++leftIndex) {

    //calculate the rightIndex
    int rightIndex = (leftIndex+1)% m_Philosophers.size();

    //send the left & right chopstick provider to the appropriate Philosopher
    Concurrency::asend(& m_Philosophers[leftIndex].LeftChopstickProviderBuffer, 
      m_ChopstickProviders[leftIndex]);
    Concurrency::asend(& m_Philosophers[leftIndex].RightChopstickProviderBuffer, 
      m_ChopstickProviders[rightIndex]);
  }
}

~Table(){
  m_ChopstickProviders.clear();
  m_Chopsticks.clear();
}

ランチの時間

最後に、main を実装するために、Philosopher のリストを宣言し、Table を作成し、Philosopher を開始し、報告メカニズムを接続し、Philosopher が食事するのを待ちます (図 6 を参照)。

図 6 食事時間

int main() {
  //create a set of Philosophers using the tr1 array
  std::tr1::array<Philosopher,5> philosophers = 
    {"Socrates", "Descartes", "Nietzsche", "Sartre", "Amdahl"};
  Table<std::tr1::array<Philosopher,5>> Table(philosophers);

  //create a list of call blocks to display
  std::vector<Concurrency::call<PhilosopherState>*> displays;

  //start the Philosophers and create display item
  std::for_each(
    philosophers.begin(),philosophers.end(),[&displays](Philosopher& cur) {

    //create a new call block to display the status
    Concurrency::call<PhilosopherState>* consoleDisplayBlock = 
      new Concurrency::call<PhilosopherState>([&](PhilosopherState in){

      //cout isn't threadsafe between each output
      if(in == Eating) 
        std::cout << cur.m_Name << " is eating\n";
      else
        std::cout << cur.m_Name << " is  thinking\n";
    });

    //link up the display and store it in a vector
    cur.CurrentState.link_target(consoleDisplayBlock);
    displays.push_back(consoleDisplayBlock);

    //and start the agent
    cur.start();
  });

  //wait for them to complete
  std::for_each(
    philosophers.begin(),philosophers.end(),[](Philosopher& cur) {

    cur.wait(&cur);
  });

  displays.clear();

  return 1;
};

Asynchronous Agents Library で提供されるエージェント クラスおよび非同期メッセージ渡しを使用して、共有状態へのアクセスを簡単に調整するにはどうすればよいか、それが明らかになっていればよいのですが。私は、明示的なロックを使わず、スレッド API を直接的に呼び出すこともなく、食事する哲学者の問題を実装できました。その実装では、整数やセマフォの配列ではなく、Chopstick、Philosopher、Table などの問題領域の用語で作業しました。

私がまだ行っていないのは、本質的な拡張性を Philosopher に追加することです。このアプリケーションは記述されたままだと、4 コアを超える CPU において処理速度がたいして速くならないでしょう。しかし、RandomSpin を別の有用なもので置き換えることで、この点を修正できます (たとえば、Parallel Pattern Library (PPL) を使用してタスクベースの並列処理で実装されるアルゴリズムなどに置き換える)。詳細については、ネイティブ同時実行に関するブログ記事をお読みになることをお勧めします。今回の記事のソース コードはこのブログから入手しました。ただし、PPL とエージェント ライブラリの構成の可能性を示すために実装する並列思考アルゴリズムは微調整しました。また、大量の並列処理で起こり得るサービスの品質の問題を管理するために、Concurrency Runtime のリソース管理機能も使用しました。処理リソースが不足した場合でも、Philosopher が食事を十分取ることが可能であることを保証します。

ご意見やご質問は mmsync@microsoft.com までお送りください。

Rick Molloy は、マイクロソフトの Parallel Computing Platform チームのプログラム マネージャです。