C# でマルチスレッドの実装を行ってみて、ハマったことや最初に知っておきたかったことのメモ。
- マルチスレッドの実装で気づいたこと
====================
1-1. スレッドの数の上限
Task.Run で同時に動くスレッドの数に限りがあり、限界に達するとタスクが空くのを待つことになる。
限界数は下記で確認可能:
ThreadPool.GetMaxThreads(out int workerThreads, out int portThreads);
Console.WriteLine("Worker threads={0}, Completion port threads={1}", workerThreads, portThreads)
上限数は下記で引き揚げ可能:
ThreadPool.SetMaxThreads(20, 20);
1-2. async/await を使うべき
できれば Task.Run
は async
/ await
とともに使うべき。
そのほうが CPU を効率よく使うように書きやすい。
1-3. 無限ループには終了条件を
Task.Run で無限ループするなら、終了フラグ見て終了する仕組み必須。
これがないと、単体テストの際にもスレッドが終わらずに困るし、システム終了時にも常に強制終了させることになる。
1-4. Sleep の代わりに Task.Delay
Thread.Sleep(500)
のかわりに await Task.Delay(500)
をつかうべき。
1-5. Task.Run の前後にログを
Task.Run
の前後にログ出力があったほうがいい。
万一、本番で待たされる事態が発生したときにログから追いかけられる。
- UnitTest(MsTest) の実装で気づいたこと
====================
2-1. テストでの無限ループの扱い
Task.Run で無限ループしていると、テストTask終了時ににそのTask(スレッド)が終わらないため(終わらないけどテストは終了するため)、複数テストを走らせた場合のみ、すぐにスレッドプールの上限に達してしまい、著しくテスト速度が劣化するという現象が発生する。
無限ループ部分はモックしてテストでは動かないようにするか、終了フラグなどで無限ループが終了する仕組みが必要。
2-2. テストは並列実行される
static
フィールドなど、テスト間で共有する領域に書き込んでいるなら、テストが並列実行されないようにロックを使った配慮が必要。
例:
using System.Threading;
public class TestsSynchronization
{
private static readonly object lockObject = new object();
public static void Lock()
{
//テストが同時に走らないためにロックを使う
Monitor.Enter(lockObject);
Console.WriteLine("----- Lock Start");
}
public static void Unlock()
{
//テストが同時に走らないためのロック解放
Monitor.Exit(lockObject);
Console.WriteLine("----- Lock End");
}
}
[TestClass]
public class MainTests
{
private static readonly object _lock = new object();
[TestInitialize]
public void TestInitialize()
{
TestsSynchronization.Lock();
}
[TestCleanup]
public void Cleanup()
{
TestsSynchronization.Unlock();
}
2-3. 書き込み可能な static フィールド
static
フィールドはテストが終了後も書き換え後の値が残っていることに注意。
必ずテスト実行時に値が初期化されるようでなければならない。
2-2. virtual を使う
メソッドの宣言で virtual
にしてないと override
できないため、そのメソッドをモックにできない。
そうなると、そのメソッドの中身を含めてテストを書かなければならず、本来テストしたいところと、そうじゃないところが混ざり込み、テストの記述が複雑になってしまう。
依存関係がテストに現れてしまう。
依存を切るために public
や protected
なメソッドは基本 virtual
にしておいたほうがいい。