こんにちは!現場でバリバリとシステムを構築していると、大量のデータを効率よくさばく必要に迫られることがよくありますよね。
「Excelのマクロや古いVB6のコードから一歩進んで、もっとモダンでパワフルな処理を書きたい!」
「マルチコアのCPUをフルに使って、重たいデータ処理をサクサク終わらせたい!」
今回は、そんなあなたに向けて、VB.NETの真骨頂とも言える「TPL Dataflow(Task Parallel Library Dataflow)」を使ったモダンなパイプライン処理の構築方法を解説します。
ここをクリアすれば、あなたのVB.NETのコードは「ただ動くもの」から「プロフェッショナルな並行処理システム」へと劇的に生まれ変わります。優しく、そして本質的なところまでしっかりと紐解いていきますので、ぜひ最後までついてきてくださいね!
—
1. なぜ「パイプライン処理」と「TPL Dataflow」なのか?
まずは、私たちが直面しがちな「よくある課題」からお話ししましょう。
例えば、数万件の巨大なCSVファイルを読み込み、データを加工(パース)し、データベースに書き込むプログラムを作るとします。これを1つのループで全部やろうとすると、どうなるでしょうか?
- ボトルネックの発生: データベースの書き込みが遅いと、データの読み込みや加工の手を止めてしまい、CPUが遊んでしまいます。
- コードのスパゲッティ化: 「読み込み」「加工」「書き込み」の処理がグチャグチャに絡み合い、どこでバグが起きているのか分からなくなります。
工場の「ベルトコンベア」をイメージしよう
これを解決するのがパイプライン処理です。工場を想像してください。
1. 工程1(データ取得): 部品をベルトコンベアに載せる人
2. 工程2(データ加工): 部品を組み立てるロボット
3. 工程3(データ保存): 完成品を箱に詰めて倉庫に運ぶ人
それぞれの工程が「独立したブロック」になり、間にあるバッファ(緩衝地帯)を通じてデータをやり取りします。これがTPL Dataflowの正体です。.NETが誇る最強の非同期・並列ライブラリの一部であり、VB.NETからでも簡単に、しかも美しく使うことができます。
—
2. 実装の全体像と準備
TPL Dataflowを使うには、プロジェクトにNuGetパッケージを追加する必要があります。以下のパッケージをインストールしておいてください。
Install-Package System.Threading.Tasks.Dataflow
今回は、次のような3段階のパイプラインをVB.NETで構築します。
1. BufferBlock(Of String): 生データを受け取り、次へ流す入口
2. TransformBlock(Of String, ProcessedData): データを並列で加工するブロック
3. ActionBlock(Of ProcessedData): 加工されたデータを最終出力(保存)するブロック
—
3. 実践!VB.NETによるパイプラインコード
百聞は一見に如かず。実際のコードを見てみましょう。
コンソールアプリケーションを想定した、そのままコピペして動かせる実用的なサンプルです。
Imports System
Imports System.Threading.Tasks
Imports System.Threading.Tasks.Dataflow
Module PipelineSample
‘ 処理結果を格納するデータ構造
Public Class ProcessedData
Public Property Id As Integer
Public Property OriginalText As String
Public Property ConvertedText As String
Public Property ProcessedAt As DateTime
End Class
Public Async Function Main() As Task
Console.WriteLine(“=== パイプライン処理を開始します ===”)
Dim stopwatch = System.Diagnostics.Stopwatch.StartNew()
‘ ==========================================
‘ ステップ1: 各ブロック(工程)の定義
‘ ==========================================
‘ ① 入力バッファブロック:データを一時的に溜める
Dim sourceBuffer = New BufferBlock(Of String)()
‘ ② 変換ブロック(Worker):並列度(MaxDegreeOfParallelism)を「4」に設定!
Dim options As New ExecutionDataflowBlockOptions With {
.MaxDegreeOfParallelism = 4 ‘ 4つのスレッドで同時に加工する
}
Dim transformer = New TransformBlock(Of String, ProcessedData)(
Async Function(input)
‘ 重い処理やAPI呼び出しをシミュレート (0.5秒待機)
Await Task.Delay(500)
Return New ProcessedData With {
.Id = CInt(Val(input)),
.OriginalText = input,
.ConvertedText = $”[加工済み] データ: {input.ToUpper()}”,
.ProcessedAt = DateTime.Now
}
End Function, options)
‘ ③ 出力(アクション)ブロック:結果をコンソールに出力(DB保存のイメージ)
Dim sinkAction = New ActionBlock(Of ProcessedData)(
Sub(data)
Console.WriteLine($”[保存完了] ID: {data.Id} | {data.ConvertedText} ({data.ProcessedAt:HH:mm:ss.fff})”)
End Sub)
‘ ==========================================
‘ ステップ2: ブロック同士をパイプ(LinkTo)で結合
‘ ==========================================
‘ FlowOptionsで「完了時に次のブロックも完了させる」を指定するのが鉄則
Dim linkOptions = New DataflowLinkOptions With {.PropagateCompletion = True}
sourceBuffer.LinkTo(transformer, linkOptions)
transformer.LinkTo(sinkAction, linkOptions)
‘ ==========================================
‘ ステップ3: データの投入(流し込み)
‘ ==========================================
For i As Integer = 1 To 10
Dim dataId = i.ToString()
‘ 非同期でバッファにデータを送り込む
AWAIT sourceBuffer.SendAsync(dataId)
Console.WriteLine($”[投入] データ {dataId} をバッファに入れました。”)
Next
‘ もうデータはないことをバッファに伝える
sourceBuffer.Complete()
‘ ==========================================
‘ ステップ4: すべての処理が終わるのを待つ
‘ ==========================================
‘ 最後のブロック(sinkAction)の完了を待機することで、パイプライン全体の見届けが可能
AWAIT sinkAction.Completion
stopwatch.Stop()
Console.WriteLine($”=== 全ての処理が完了しました (所要時間: {stopwatch.ElapsedMilliseconds} ms) ===”)
End Function
End Module
—
4. コードの深掘り:ここがエンジニアの腕の見せ所!
上記のコードで、VB.NET初学者やマクロ出身者が「おっ」と驚くポイント、そして実務でハマりやすい重要ポイントを解説します。
① `MaxDegreeOfParallelism = 4` の魔力
`TransformBlock` のオプションに `MaxDegreeOfParallelism` を指定しています。
これにより、「データを加工する処理だけを自動的に4つのスレッドに分散して並列実行」してくれます。もしこれがシングルスレッドなら10件×0.5秒で5秒かかるところが、並列化によって圧倒的なスピードで処理されます。CPUコアを効率よく使い倒すモダンな手法です。
② `LinkTo` と `PropagateCompletion`
ブロックとブロックを繋いでいるのが `sourceBuffer.LinkTo(transformer, linkOptions)` です。
ここでミソなのが `PropagateCompletion = True`。これがないと、「入口のバッファが終わったよ」という通知が次のブロックに伝わらず、プログラムが永遠に終了待ち(デッドロック状態)になってしまいます。連結する際はセットで覚えておきましょう。
③ 非同期の基盤 (`Async / Await`)
VB.NETでも `Async` と `Await` を使うことで、スレッドをブロックせずに効率的なI/O処理が書けます。TPL Dataflowは非同期処理との相性が抜群に良いです。
—
5. 陥りやすいエラーと注意点
現場でこのアーキテクチャを使う際、初心者が必ずと言っていいほど踏む地雷がいくつかあります。
1. 例外のハンドリング(エラー処理)を忘れる
- パイプライン途中のブロックで例外が発生した場合、その例外は `Completion` プロパティにカプセル化されます。各ブロックの `Completion` タスクに対して `Try-Catch` を行うか、エラー専用の処理ブロックを挟む設計を必ず行いましょう。
2. UIスレッド(Windows Forms / WPF)でのデッドロック
- ボタンクリックイベントなどからこの処理を呼び出す際、`AWAIT` の戻り先(コンテキスト)が原因でUIがフリーズすることがあります。バックグラウンドで動かしたい場合は `.ConfigureAwait(False)` を適切に挟む配慮が必要です。
—
まとめ
今回は、VB.NETにおけるTPL Dataflowを用いたパイプライン処理の構築法を解説しました。
- データ加工を独立したブロック(部品)に分けることで、保守性が劇的に向上する。
- バッファと並列度 (`MaxDegreeOfParallelism`) を制御することで、パフォーマンスを限界まで引き出せる。
- `LinkTo` と `PropagateCompletion` で流れと終了を美しく管理する。
ここをクリアできれば、あなたのVB.NETスキルはもう「初心者・マクロの延長」ではありません。自信を持ってプロフェッショナルなバックエンド処理を設計できるようになります。
日々の開発業務のパフォーマンス改善に、ぜひこの「パイプライン思考」を取り入れてみてくださいね。それでは、また次回の技術知見でお会いしましょう!
