本書はタイトルの通り C# の非同期/並列処理についての書籍です.

対象としているのはマルチスレッドの理論的な事柄ではありません. .NET や C# にすでに備わっている環境をどのように利用してプログラムを書くかということに焦点を当てています.
そのため, 具体的な.NET のライブラリの使い方を, 実践を通じて知りたい方におすすめです.

個人的な白眉は昔のバージョンから新しいバージョンまで, 各 .NET での実装が比較されている点です. バージョン 1.x 系の.NET ではこう書く, 2.x 系ではこう書く, といった風にして, 1.x 系から 4.5 まで扱います.
今更昔のバージョンの .NET を利用することはないとはいえ, 歴史的な変遷を見られてためになりました. 昔はこんなに面倒だったのかと驚き, それによって新しいバージョンでの書き方では何が省略されているのかイメージを掴むことができました.

非同期/並列処理の基礎

まずは非同期/並列処理とは何か, ということから始まります. マルチスレッド, レースコンディション, ロック, スレッド間同期といった主要なトピックスが紹介されています.
この辺りのことはすでに一通り知っていたので新鮮さはなかったのですが, 同じことでも別の説明を読むのはためになります.

いいなと思ったのは「非同期」と「並列」という言葉の使い分けです. 「並行」と「並列」が一般的ですが, これらは混同しやすいので本書では並行の代わりに非同期という言葉が使われています. 個人的にも「並行」と「並列」は言葉だけだといつも分からなくなるので「concurrent」と「parallel」で覚えていますが, 「非同期」と「並列」の方が分かりやすくて良いかもしれません.

新旧 .NET で変遷を見る

例題を各バージョンの .NET で実装して違いを見る章があります.
例題は「1-10 の数字を 3 桁に 0 埋めして画面に表示する」です. マルチスレッドで並列に計算することと, 画面をフリーズさせないように計算を非同期で行うという 2 つ問題を含んだ題材です.

これを最初に見たとき, なぜこんな簡単なものが例題になりうるのだろうと思いました. 計算はAsParallel()を使って簡単に実装できます.

1
2
3
4
Enumerable.Range(1, 10)
  .AsParallel()
  .Select(x => x.ToString("000"))
  .ForAll(s => Console.WriteLine($"{s} ({Thread.CurrentThread.ManagedThreadId})"));

出力を見ると, ちゃんと複数スレッドで順不同に実行されていることが確認できます.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
007 (10)
001 (8)
004 (16)
002 (9)
010 (19)
005 (11)
006 (12)
009 (7)
008 (20)
003 (1)

GUI をフリーズさせないためには async/await を使えば良いでしょう.

こんなに簡単なことを数十ページも使って説明することがあるのだろうかと思ったのですが, .NET 1.x 系の例を見て驚きました. 何やら色々書いてあって, 100 行は超えていそうです.
ThreadPoolを使った並列処理はまだいいとして, スレッドの終了待ちがかなり面倒です. スレッドの処理が終了するたびにカウンタを (スレッドセーフに) デクリメントして, カウンタが 0 になるのを待ちます. カウンタの状態をポーリングするためにタイマーで定期的に割り込むとか, コールバックを渡すとかいった方法が紹介されています (もちろん本書には丁寧な説明があります).

眺めてみると, 要は

  • スレッドの生成
  • 実行する関数のスレッドへの割り当て
  • スレッドの終了待ち

といったことを行っています. 上記の PLINQ を使った例では隠蔽されていますが, 裏でこういったことをよしなにやってくれているということです.

バージョン 2.x 系, 3.x 系なども紹介されていますが, 若干便利になっただけで似たようなものだと感じました. 4 以降から Task や Task Parallel Library の登場によってモダンな書き方が可能になりました.
見比べてみるとかなりの進化です. 着実に進化していて.NET は凄いです.

ラングトンのループ

本書の締めくくりとして「ラングトンのループ」というものが題材に挙げられています. これはセル・オートマトンの一種です.
セル・オートマトンというとライフゲームは知っていました. 格子状に区切られたマス目のそれぞれが 0/1 の状態を持ち, いくつかのルールに従って近傍のマス目との関係によって状態を変化させると, シンプルなルールから驚くほど複雑なパターンが生まれるというものです (ご存知ない方には Wikipedia第一学習社のページ がおすすめ).

ラングトンのループはライフゲームを複雑にしたものです. セルが 8 つの状態を持ち, 状態変化のルールが 200 個以上あります. そして適切な初期状態を定めると, 自己複製するループが現れます.
このラングトンのループをあの手この手で高速化するという流れで, 実際の並列化の雰囲気がつかめます. マルチスレッドは複雑でバグも起きやすいので, 最初はシングルスレッドでシンプルに実装, その後必要があれば手を入れていくというやり方は念頭に置きたいです.

高速化の流れも読んでいて面白かったですが, それよりも「自己複製するループ」という響きが神秘的で見た目も美しく, これはぜひ自分でも実装してみたいと思いました (興味の対象がもはや非同期/並列とは関係ないですが).

Unity で実装したものを以下に載せておきます.
実装にあたって, ルールは こちらで公開されているファイル を利用させていただきました.

out

栄養を運んで伸長しているように見えたので, 植物や自然をイメージした配色にしました (ラングトン自らによるプレゼン動画ではもっとポップな見た目です).
ループは隣にループを複製し, 複製を終えると動きのない固定パターンに落ち着きます.


本書の内容と無関係なので完全に余談ですが, Linux で Unity の環境を整えるのが地味に面倒でした. インストールと起動は特に問題もなくスムーズでしたが, Visual Studio Code で補完を効かせるために一手間必要でした.

  1. ラングトンのループがやりたい
  2. GUI のアプリが作れる環境が必要
  3. Unity をインストール
  4. エディタなどの環境を整える
  5. Unity のチュートリアルをやる
  6. ようやくラングトンのループを実装できる

本来の目的にたどり着くまでの無駄に長い道のりは, まさに yak shavinig という感じでした. しかし Unity 自体は良いものだという雰囲気を感じました. 公式のチュートリアルも充実していて親切ですし, Linux でも使えますし, 数年前に少し触った頃から UI が変わって見やすくなっていました. 本格的に学ぶとまでは行かなくても, 視覚的なものを作るときの選択肢として大いに有効だと思いました.

ちなみに, 本書では WinForm や WPF が使われていますが Linux では使えないので候補外でした. .NET Maui なら行けるかもと思いましたが, 少し悩んだ後に今回は Unity でやることにしました. .NET Maui も気になっているのでいつか触ってみるかもしれません.

終わりに

「C# の非同期/並列処理」というテーマを概観するには良い本だったと思います. 非同期/並列の基礎から使い方まで載っていて実践的です.
バージョンを横断的に眺めることで, 新しい (と言っても 10 年前くらいですが) .NET ではいかに簡単にコードが書けるかということが分かりました. 結局のところ, 殆どの場合は async/await や Parallel などの便利なものを使っておけば良くて, 逆にそれらを使わずにごちゃごちゃとコードを書いていたら, もっと良いやり方がある可能性がありそうです.

そして本書の主題ではありませんが, 個人的にはラングトンのループを実装していて楽しかったです. おしゃれな題材を扱ってくれた筆者に感謝です.