デベロッパー

デベロッパー

.NET Framework 4.0 Task Parallel Library のタスクを理解する

Jeffrey Juday
2010年8月31日 / 10:00
 
 

はじめに

Microsoft は「Microsoft Visual Studio 2010」と「.NET Framework 4.0」で並列コンピューティング機能に多大な投資をしてきた。新機能を理解するには、核となる機能のコンポーネントを調べて核となる機能やコンセプトを見つけ出すことが最適な方法である場合が多い。さらに深く掘り下げ、各所で見られる一連のコンポーネントセットを見つけ出す。「Task Parallel Library」(TPL)は.NET Framework の並列コンピューティングで核となる機能だ。この Task クラスこそ TPL の心臓部である。Task クラスを使って TPL を活用する方法を以下で説明していく。

新しいモデル

開発者は Microsoft Visual Studio や.NET Framework に数バージョン前から搭載されてきた機能によって TPL を活用することができるが、TPL のすべてのメリットを本当に享受するには多くの.NET 開発者にとってあまり馴染みのないアーキテクチャ的アプローチが必要になる。

筆者は、TPL はスレッド、デレゲート、そしてスレッドプールといったおなじみのコンセプトを容易に拡張する新しいタイプのモデルに従っていると思う。モデルは、規範的なパターンに従って問題を解決し、特定の前後関係において機能する。新しいモデルを理解するための最初のステップは、その新しいモデルの背景にある概念に精通することだ。基本的な考え方は次のようになる。
  • デレゲートはファンクションに対する参照だ。デレゲートは、クラスへの完全なリファレンスを渡すことなくアプリケーションの中で回していくことができる。開発者は、デレゲートを含んだデータ構造を整理して呼び出すことにより、アプリケーションのフローをコントロールすることができる。
  • デレゲートは、スレッドプールのスレッドかアプリケーション内で作成されたスレッドに渡すことができる。開発者は複数のスレッドを使ってアプリケーションのパフォーマンスと応答性を向上させる。
  • スレッドは同時に動作することがあり、同時に動作する2つのスレッドがデータ構造を共有することが多いため、実行中のデレゲートは共有データ構造のアクセス方法をコントロールする必要がある。実行中のデレゲートは、データ構造を変更する前に合図を送るか共有データ構造をロックする必要がある。
  • データ構造をロックし、スレッドを多く作成しすぎると、ボトルネックが発生したり、アプリケーションのリソースを消費してしまうことになり、マルチスレッドの目的にそぐわなくなってしまう。
TPL の適用方法を完全に把握するにはコードが必要になる。本稿の残りの部分では、ほかの TPL クラスとタスクの連携内容を紹介する TPL のサンプルコードを詳しく見ていく。

TPL コアとサンプルコード

多数の TPL クラスをカプセル化するネームスペースは2つある。「System.Threading.Tasks」と「System.Collections.Concurrent」だ。ネームスペースのなかで出てくるクラスの一覧を以下にいくつか紹介する。
  • Task
  • TaskScheduler
  • TaskFactory
  • TaskCancelledException
  • BlockingCollection<T>
以下のコードは、Microsoft Parallel Computing サイトにある「Patterns of Parallel Programming」(並行プログラミングの各種パターン)という白書の55ページから引用したもので、各ネームスペースのクラスを利用する。
static void ProcessFile(string inputPath, string outputPath)
{
    var inputLines = new BlockingCollection<string>();
    var processedLines = new BlockingCollection<string>();
    // Stage #1
    var readLines = Task.Factory.StartNew(() =>
    {
        try
        {
            foreach (var line in File.ReadLines(inputPath)) inputLines.Add(line);
        }
        finally { inputLines.CompleteAdding(); }
    });
    // Stage #2
    var processLines = Task.Factory.StartNew(() =>
    {
        try
        {
            foreach (var line in inputLines.GetConsumingEnumerable()
            .Select(line => Regex.Replace(line, @"\s+", ", ")))
            {
                processedLines.Add(line);
            }
        }
        finally { processedLines.CompleteAdding(); }
    });
    // Stage #3
    var writeLines = Task.Factory.StartNew(() =>
    {
        File.WriteAllLines(outputPath, processedLines.GetConsumingEnumerable());
    });
    Task.WaitAll(readLines, processLines, writeLines);
}
ご覧の通り、これは興味深いコードだ。一見シーケンシャルに見えるものの、本稿のテーマは並列コンピューティングである。これまでにマルチスレッドアプリケーションを見たこと、もしくは書いたことがある方は、このような構造のプログラムはおそらく見慣れていないのではないだろうか。TPL は、マルチスレッドおよび非同期コードで見られる見苦しい部分の大半を隠してくれるのだ。

さて、ここでこのコードの仕組みを分析していこう。先に約束したように、まずは Task クラスの役割から解説する。

タスク

開発者は、タスクを「新しくする」もしくは TaskFactory を利用してタスククラスをインスタンス化することができる。タスクは Action デレゲートの実行をラップする。相互依存の Action デレゲートを非同期に呼び出すには何らかの調整が必要になる。Action デレゲートは現行のスレッドもしくは考えられるほかの複数のスレッド上でも非同期に実行することができる。そこで、開発者は実行スレッドを指定する代わりにデレゲート(ワーク)を整理し、ワークの相互依存性を宣言して、ほかの TPL クラスのグループに実行を任せる。 Task クラスメソッドは、それぞれが実行のコーディネーションテーマを持つメソッド過負荷のグループを明らかにする。グループ分けは以下のようになる。
  • Start - タスクに Action の実行開始をスケジューリングさせる。
  • Wait - Action が実行を完了し、タスクが Action の完了通知を受け取るまで処理をブロック。
  • WaitAll - Wait と同様の動作をするが、タスクコレクションで中断し、すべてのタスクの完了を待つ。
  • WaitAny - Wait と同様の動作をするが、コレクション中のタスクの1つが完了するまで処理を中断する。
  • ContinueWith - タスクが Action の完了通知を受け取ったときに別のタスクを実行し、開発者が依存しているタスク同士を結びつけられるようにする。一部の関連資料では、「Continuation」とも呼ばれている。
タスクの作成と実行は以下の例に含まれている。
    var readLines = Task.Factory.StartNew(() =>
    {
        try
        {
            foreach (var line in File.ReadLines(inputPath)) inputLines.Add(line);
        }
        finally { inputLines.CompleteAdding(); }
    });
この例は TaskFactory を利用している。TaskFactory は特定の規則に従う Task の作成を簡略化する。この例で選択された TaskFactory の規則は新しいタスクをデフォルトの TaskScheduler で作成し、Task Start メソッドをコールする。

TaskScheduler はタスクをキューイングして一連のスレッド上で動かす作業を管理する。TaskScheduler の動作はスレッドプールと非常に良く似ており、独自の TaskScheduler を作成し、さまざまなタイプのコーディネーションや作業負荷を処理し、デフォルトをカスタム TaskScheduler にスワップアウトできるのは開発者だけとなっている。

サンプルコードは、読みやすさを向上させる匿名メソッドの使用方法も紹介している。

だが特に、この例は開発者が相互依存のデレゲートを Task オブジェクトにまとめて、同時処理を保証するデータ構造を使ってコーディネートし、サンプルコードが完了するまで次から次へタスクを実行していく方法を示している。

BlockingCollection

本稿の前半で紹介したサンプルコードのコピーをもう1つ紹介する。
static void ProcessFile(string inputPath, string outputPath)
{
    var inputLines = new BlockingCollection<string>();
    var processedLines = new BlockingCollection<string>();
    // Stage #1
    var readLines = Task.Factory.StartNew(() =>
    {
        try
        {
            foreach (var line in File.ReadLines(inputPath)) inputLines.Add(line);
        }
        finally { inputLines.CompleteAdding(); }
    });
    // Stage #2
    var processLines = Task.Factory.StartNew(() =>
    {
        try
        {
            foreach (var line in inputLines.GetConsumingEnumerable()
            .Select(line => Regex.Replace(line, @"\s+", ", ")))
            {
                processedLines.Add(line);
            }
        }
        finally { processedLines.CompleteAdding(); }
    });
    // Stage #3
    var writeLines = Task.Factory.StartNew(() =>
    {
        File.WriteAllLines(outputPath, processedLines.GetConsumingEnumerable());
    });
    Task.WaitAll(readLines, processLines, writeLines);
}


タスクの役割はある程度直感的だが、「BlockingCollection」の役割は多くの.NET 開発者にとって新しい初めての内容だろうと思う。以下の図は、「inputLines」から「processedLines」までのデータの流れと、それがどのようにファイルに書き込まれるのかを示している。

右側の処理はすべて同時に実行され、「foreach」命令文は一連のコードを経由してデータを引っ張り出すことを覚えておきたい。したがって、「inputLines」が一番上に追加されると「processedLines」がコレクションから削除され、出力ファイルに書き出される。

BlockingCollection」は前述のようなタイプの作業に理想的な多数の機能を搭載している。

前述の通り、ロッキングやシグナリグなどのさまざまなスレッド対応機能は、共有データ構造にとって重要なものだ。 「BlockingCollection」は、スレッド対応で(同じように重要なことだが)効率的にこれらすべてを処理する。 2番目に、 「GetConsumingEnumerable」メソッドは「 returns an IEnumerable<T>」インターフェースを返す。したがって、上のサンプルコードでは、ご覧のように「BlockingCollection」は LINQ や「foreach」命令文への対応に優れる。

図1:BlockingCollectionのタスク

追加

ここまで多数の高度な Task クラス機能を簡単に見てきた。それらの機能の一部を以下で解説する。

Asynchronous Programming Model (APM)はアプリケーションの応答性に欠くことができない。APM は.NET Framework全体を通じて利用されている。「ParallelExtensionsExtras」のサンプルには.NET の APM 機能の大半のサポートに加え、TPL クラスによってまだカプセル化されていないパターンも含まれている。

タスクのキャンセルを処理するクラスも複数ある。導入すると、キャンセルは「try {} catch {}」命令文を使って例外を処理するのと同じように処理される。

通常、Microsoft が.NET Frameworkに新機能を追加するときは、ツール関係は後のリリースで用意される。.NET 4.0と Visual Studio 2010では、Microsoft はフレームワーク機能と多彩なツールを追加している。

最後になるが、並列コンピューティングのマニュアルは明確で、良く書かれており、内容も広範に及ぶ。以下で紹介するサイトをご覧になることをぜひお勧めする。そこでは、サンプルやパターン、そしてガイドラインまで、さまざまなものが見つかることだろう。

まとめ

開発者がマルチコアを修得していくのであれば、まずは並列コンピューティングの新しいテクニックとデータ構造を修得する必要がある。Task Parallel Library (TPL)は.NET Framework における並列コンピューティングの核となっている。TPL の修得はまず Task クラスを理解することから始まる。

参考

並列コンピューティングの開発サイトには必要なものすべてが揃っている。
並列コンピューティングのパターン:並列パターンを.NET Framework 4.0や Visual C#と一緒に理解および応用する
.NET Framework 4.0対応の並列コンピューティングのサンプル
ParallelExtensionsExtras を見る
マルチコアマシンでの管理コードの最適化
Microsoft Parallel Computing 開発センターにある並列コンピューティング関連の技術記事
PDC 2009関連ビデオ
TPL 関連資料

関連記事

著者紹介

Jeffrey Juday(Jeffrey Juday)
Jeff は BizTalk、SharePoint、WCF、WF、および SQL Server を利用したエンタープライズアプリケーション統合ソリューションが専門のソフトウェア開発者。Jeff は、軍事、製造、金融サービス、経営コンサルティング、およびコンピュータセキュリティなど、さまざまな業界で Microsoft のツールを使って15年以上ソフトウェアの開発を経験してきた。Jeff は Microsoft BizTalk MVP でもある。Jeff は、余暇は妻の Sherrill や娘の Alexandra と過ごす。
【関連記事】
クラウドアプリケーション開発用の無償 Microsoft Azure SQL ツール
国内総合宿泊予約サイト「じゃらんnet」iPhone アプリが機能拡充
ASP.NET 開発者のための10の素晴らしい CodePlex プロジェクト
Twitter 世代に嫌われた民主党――Twitter 模擬選挙結果速報
参院選 2010 twitter 模擬選挙、「Good Net Voting」が開始

New Topics

Special Ad

ウマいもの情報てんこ盛り「えん食べ」
ウマいもの情報てんこ盛り「えん食べ」 「えん食べ」は、エンジョイして食べる、エンターテイメントとして食べものを楽しむための、ニュース、コラム、レシピ、動画などを提供します。 てんこ盛りをエンジョイするのは こちらから

Hot Topics

IT Job

Interviews / Specials

Popular

Access Ranking

Partner Sites