Closed10
More Effective C# 6.0/7.0 のメモ(第4章)
項目35 PLINQによる並列アルゴリズムの実装を学ぼう
パーティション分割
範囲(range)パーティショニング
- 最も単純。入力シーケンスをタスク(コア数)の数で割る。
- クエリのソースがシーケンスのインデックスをサポートしていて、そのシーケンスに要素が何個あるのかを報告してくれる場合(List<T>、配列等)に限られる。
チャンクによるパーティショニング
- アルゴリズムの詳細は変化する。
- タスクに対して、まずは小さな集合を割り当て(数が小さなシーケンスの処理を1つのタスクが担当しないようにするため)、処理が進むに従ってサイズを大きくしていく(スレッディングのオーバーヘッドを最小にするため)。
ストライプ化パーティショニング
- 範囲パーティショニングの特殊なケース
- ワーカータスクが4本のとき、第1のタスクが0, 4, 8, 12、第2のタスクが1, 5, 9, 13を受け持つなど。
ハッシュによるパーティショニング
- Join, GroupBy, Distinctなどの演算を行うクエリのために設計された特殊用途のアルゴリズム。
タスクの並列化に用いるアルゴリズム
パイプライン化
- 1本のスレッドで列挙の処理を行う。個別の要素に対するクエリ処理には複数のスレッドが使われる。
- 例えば16コアのマシンなら、最初の16個の要素が即座に16本のスレッドで処理される。これプラス列挙用のスレッドが1本ある。(訳が読みにくくてたぶんそう?)
ストップ&ゴー
- ToList()やToArray()で、クエリの即時実行を要求する時や、PLINQが順序付けなどの、処理を続ける前に結果の完全な集合を必要とするときに用いられる。
- わずかな性能向上がえられるが、代わりにメモリを多く利用することになる。
逆列挙
- 通常のLINQ to Objectsのように、遅延評価されるモデルを採用したいとき。
- ForAll()を利用する。
AsParallel()を使って並列になる例
public class Program
{
public static void Main()
{
// 1から100までのリスト
var nums = Enumerable.Range(1, 100).ToList();
var results = nums
.Select(n => $"{n}, {Thread.CurrentThread.ManagedThreadId}");
foreach (var item in results)
{
Console.WriteLine(item);
}
}
}
出力
1, 1
2, 1
3, 1
4, 1
5, 1
6, 1
7, 1
8, 1
9, 1
10, 1
11, 1
12, 1
/// 以下省略
- 並列にしてみる
var results = nums.AsParallel()
.Select(n => $"{n}, {Thread.CurrentThread.ManagedThreadId}");
出力
21, 12
31, 13
41, 14
51, 15
61, 16
71, 17
81, 18
91, 11
1, 11
11, 8
22, 12
32, 13
42, 14
52, 15
62, 16
72, 17
82, 18
92, 11
2, 11
12, 8
23, 12
33, 13
43, 14
- resultsの型が、
ParallelQuery<string>
と推論されている。
パーティション分割
数値 | 1~10 | 11-20 | 21-30 | 31-40 | 41-50 | 51-60 | 61-70 | 71-80 | 81-90 | 91-100 |
---|---|---|---|---|---|---|---|---|---|---|
スレッド | 11 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 11 |
ForEach(直列)とAsParallel.ForAll(並列)での実行時間の差
public class Program
{
public static void Main()
{
// 1から100までのリスト
var nums = Enumerable.Range(1, 100).ToList().ToList();
var sw = new System.Diagnostics.Stopwatch();
sw.Restart();
nums.ForEach(n =>
{
Thread.Sleep(10);
Console.WriteLine($"{n}, {Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds}ms");
sw.Restart();
nums.AsParallel().ForAll(n =>
{
Thread.Sleep(10);
Console.WriteLine($"{n}, {Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds}ms");
}
}
- 出力
- 並列の方が圧倒的に時間が短い
1, 1
2, 1
3, 1
4, 1
...省略
98, 1
99, 1
100, 1
Elapsed: 1195ms
21, 14
1, 8
61, 18
71, 19
...省略
70, 18
10, 8
90, 20
100, 1
Elapsed: 124ms
項目35(続き)
- LINQ to Objectsでは、遅延実行であり、その要素が要求されるとその要素に対するクエリを実行する。
- PLINQはそうではなく、最初の要素が要求されたときに結果のシーケンス全体が生成される。
- この違いについて理解していないと、並列クエリを実行したときの方がLINQ to Objectsクエリよりも遅くなってしまうことがある。
普通のLINQ to Objects
- クエリの実行は、最初のMoveNext()が呼び出されるまで開始されない。
- 1つ1つ、次の出力要素を生成するのに必要な数の要素に対するクエリを実行していく。
public static class Program
{
public static void Main()
{
var nums = Enumerable.Range(0, 300).ToList().ToList();
var answers = nums
.Where(n => n.SomeTest())
.Select(n => n.SomeProjection());
var iter = answers.GetEnumerator();
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, 列挙の開始");
while (iter.MoveNext())
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, MoveNext(), Current: {iter.Current}");
}
}
private static bool SomeTest(this int input)
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, テスト中の要素: {input}");
return input % 3 == 0;
}
private static string SomeProjection(this int input)
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, プロジェクション中の要素: {input}");
return $"スレッド: {Thread.CurrentThread.ManagedThreadId}, 戻り値の文字列: {input} at {DateTime.Now}";
}
}
出力
スレッド: 1, 列挙の開始
スレッド: 1, テスト中の要素: 0
スレッド: 1, プロジェクション中の要素: 0
スレッド: 1, MoveNext(), Current: スレッド: 1, 戻り値の文字列: 0 at 2024/04/21 17:56:29
スレッド: 1, テスト中の要素: 1
スレッド: 1, テスト中の要素: 2
スレッド: 1, テスト中の要素: 3
スレッド: 1, プロジェクション中の要素: 3
スレッド: 1, MoveNext(), Current: スレッド: 1, 戻り値の文字列: 3 at 2024/04/21 17:56:29
スレッド: 1, テスト中の要素: 4
スレッド: 1, テスト中の要素: 5
スレッド: 1, テスト中の要素: 6
スレッド: 1, プロジェクション中の要素: 6
スレッド: 1, MoveNext(), Current: スレッド: 1, 戻り値の文字列: 6 at 2024/04/21 17:56:29
スレッド: 1, テスト中の要素: 7
スレッド: 1, テスト中の要素: 8
スレッド: 1, テスト中の要素: 9
スレッド: 1, プロジェクション中の要素: 9
...省略
スレッド: 1, テスト中の要素: 292
スレッド: 1, テスト中の要素: 293
スレッド: 1, テスト中の要素: 294
スレッド: 1, プロジェクション中の要素: 294
スレッド: 1, MoveNext(), Current: スレッド: 1, 戻り値の文字列: 294 at 2024/04/21 18:01:57
スレッド: 1, テスト中の要素: 295
スレッド: 1, テスト中の要素: 296
スレッド: 1, テスト中の要素: 297
スレッド: 1, プロジェクション中の要素: 297
スレッド: 1, MoveNext(), Current: スレッド: 1, 戻り値の文字列: 297 at 2024/04/21 18:01:57
スレッド: 1, テスト中の要素: 298
スレッド: 1, テスト中の要素: 299
並列バージョン
- MoveNext()が一番最初に呼び出される時、PLINQは結果の生成に関わるすべてのスレッドを指導する。それによってかなりの数の結果オブジェクトが生成される。
- その後で呼び出されるMoveNext()は、すでに生成された結果から次の要素を取り出す。
var answers = nums.AsParallel() // AsParallelをつけるだけの変更
.Where(n => n.SomeTest())
.Select(n => n.SomeProjection());
出力
スレッド: 1, 列挙の開始
スレッド: 8, テスト中の要素: 30
スレッド: 14, テスト中の要素: 90
スレッド: 15, テスト中の要素: 120
スレッド: 11, テスト中の要素: 0
スレッド: 16, テスト中の要素: 150
スレッド: 13, テスト中の要素: 60
スレッド: 17, テスト中の要素: 180
スレッド: 17, プロジェクション中の要素: 180
スレッド: 8, プロジェクション中の要素: 30
スレッド: 11, プロジェクション中の要素: 0
スレッド: 16, プロジェクション中の要素: 150
スレッド: 14, プロジェクション中の要素: 90
スレッド: 13, プロジェクション中の要素: 60
スレッド: 15, プロジェクション中の要素: 120
スレッド: 18, テスト中の要素: 210
スレッド: 18, プロジェクション中の要素: 210
スレッド: 19, テスト中の要素: 240
スレッド: 19, プロジェクション中の要素: 240
スレッド: 13, テスト中の要素: 61
スレッド: 8, テスト中の要素: 31
スレッド: 14, テスト中の要素: 91
スレッド: 14, テスト中の要素: 92
...省略
スレッド: 15, プロジェクション中の要素: 132
スレッド: 14, テスト中の要素: 110
スレッド: 15, テスト中の要素: 133
スレッド: 1, MoveNext(), Current: スレッド: 8, 戻り値の文字列: 30 at 2024/04/21 18:03:17
スレッド: 14, テスト中の要素: 111
スレッド: 15, テスト中の要素: 134
スレッド: 14, プロジェクション中の要素: 111
スレッド: 15, テスト中の要素: 135
スレッド: 14, テスト中の要素: 112
スレッド: 1, MoveNext(), Current: スレッド: 8, 戻り値の文字列: 33 at 2024/04/21 18:03:17
スレッド: 15, プロジェクション中の要素: 135
スレッド: 1, MoveNext(), Current: スレッド: 8, 戻り値の文字列: 36 at 2024/04/21 18:03:17
スレッド: 14, テスト中の要素: 113
スレッド: 1, MoveNext(), Current: スレッド: 8, 戻り値の文字列: 39 at 2024/04/21 18:03:17
スレッド: 14, テスト中の要素: 114
スレッド: 1, MoveNext(), Current: スレッド: 8, 戻り値の文字列: 42 at 2024/04/21 18:03:17
スレッド: 14, プロジェクション中の要素: 114
スレッド: 1, MoveNext(), Current: スレッド: 8, 戻り値の文字列: 45 at 2024/04/21 18:03:17
スレッド: 14, テスト中の要素: 115
スレッド: 1, MoveNext(), Current: スレッド: 8, 戻り値の文字列: 48 at 2024/04/21 18:03:17
スレッド: 14, テスト中の要素: 116
スレッド: 14, テスト中の要素: 117
スレッド: 14, プロジェクション中の要素: 117
...省略
スレッド: 1, MoveNext(), Current: スレッド: 8, 戻り値の文字列: 297 at 2024/04/21 18:03:17
スレッド: 1, MoveNext(), Current: スレッド: 11, 戻り値の文字列: 24 at 2024/04/21 18:03:17
スレッド: 1, MoveNext(), Current: スレッド: 16, 戻り値の文字列: 177 at 2024/04/21 18:03:17
スレッド: 1, MoveNext(), Current: スレッド: 17, 戻り値の文字列: 207 at 2024/04/21 18:03:17
スレッド: 1, MoveNext(), Current: スレッド: 19, 戻り値の文字列: 261 at 2024/04/21 18:03:17
スレッド: 1, MoveNext(), Current: スレッド: 11, 戻り値の文字列: 27 at 2024/04/21 18:03:17
スレッド: 1, MoveNext(), Current: スレッド: 19, 戻り値の文字列: 264 at 2024/04/21 18:03:17
スレッド: 1, MoveNext(), Current: スレッド: 19, 戻り値の文字列: 267 at 2024/04/21 18:03:17
項目36 並列アルゴリズムは例外を考慮して構築しよう
- 非同期処理から返却されるAggregateExceptionは、そのInnerExceptionsの中にさらにAggregateExeptionを含む可能性がある。そのため、再帰的に中へ中へ調査していく必要がある。
- 通常のLINQであれば、クエリの定義自体をtry / catchで囲う必要はない。しかしPLINQでは囲う必要がある。
- PLINQは即時評価ではないが、スケジューラが許す限り速やかに結果の生成が始動されるため。
AggregateExceptionを処理するコード
private static bool HandleAggregateError(
AggregateException aggregate,
Dictionary<Type, Action<Exception>> exceptionHandlers)
{
foreach (var exception in aggregate.InnerExceptions)
{
if (exception is AggregateException agEx)
{
// 再帰的に処理する
if (!HandleAggregateError(agEx, exceptionHandlers))
{
return false;
}
}
else if (exceptionHandlers.ContainsKey(exception.GetType()))
{
// もし処理可能な型として登録されていれば、そのとおりに処理する
exceptionHandlers[exception.GetType()](exception);
}
else
{
// それ以外の例外は処理できないので、falseを返す
return false;
}
}
// 全ての例外が想定通りに処理できた場合
return true;
}
項目37 スレッドを作る代わりにスレッドプールを使おう
- スレッドプールの仕組みによって、一度作ったスレッドが空いたら再び利用できるようにしてくれるため、自分でスレッドを新規に作成するよりもオーバーヘッドが小さい。
方式 | 1 | 3 | 5 | 10 | 15 | 20 | 30 | 40 | 50 |
---|---|---|---|---|---|---|---|---|---|
OneThread | 95 | ||||||||
Thread Pool | 2 | 2 | 3 | 6 | 8 | 10 | 13 | 19 | 21 |
ManualThread | 3 | 3 | 3 | 6 | 15 | 22 | 30 | 40 | 40 |
実験コード
using System.Diagnostics;
public static class Program
{
public static void Main()
{
// Console.WriteLine("One thread: " + OneThread());
Console.WriteLine("Thread Pool: " + ThreadPoolThreads(50));
// Console.WriteLine("ManualThreads: " + ManualThreads(50));
}
private static double OneThread()
{
var sw = new Stopwatch();
sw.Start();
for (int i = 1; i <= 1000000; i++)
{
Helo.FindRoot(i);
}
sw.Stop();
return sw.ElapsedMilliseconds;
}
private static double ThreadPoolThreads(int numThreads)
{
var sw = new Stopwatch();
using (AutoResetEvent e = new AutoResetEvent(false))
{
int workerThreads = numThreads;
sw.Start();
for (int thread = 0; thread < numThreads; thread++)
{
ThreadPool.QueueUserWorkItem((x) =>
{
for (int i = 1; i < 1000000; i++)
{
if (i % numThreads == thread)
{
Helo.FindRoot(i);
}
}
if (Interlocked.Decrement(ref workerThreads) == 0)
{
e.Set();
}
});
}
e.WaitOne();
sw.Stop();
return sw.ElapsedMilliseconds;
}
}
private static double ManualThreads(int numThreads)
{
var sw = new Stopwatch();
using (AutoResetEvent e = new AutoResetEvent(false))
{
int workerThreads = numThreads;
sw.Start();
for (int thread = 0; thread < numThreads; thread++)
{
Thread t = new Thread(() =>
{
for (int i = 1; i < 1000000; i++)
{
if (i % numThreads == thread)
{
Helo.FindRoot(i);
}
}
if (Interlocked.Decrement(ref workerThreads) == 0)
{
e.Set();
}
});
t.Start();
}
e.WaitOne();
sw.Stop();
return sw.ElapsedMilliseconds;
}
}
}
public static class Helo
{
public static double FindRoot(double number)
{
double guess = 1;
double error = Math.Abs(guess * guess - number);
while (error > 0.000000001)
{
guess = (number / guess + guess) / 2.0;
error = Math.Abs(guess * guess - number);
}
return guess;
}
}
項目38 スレッド間通信にはBackgroundWorkerを使おう
-
QueueUserWorkItem
メソッドには、「エラーの報告」や「タスクの完了や進捗を検知する」「タスクの一時停止やキャンセルに役立つメソッド」が存在しない。 -
BackgroundWorker
コンポーネントにはこれらが含まれている。
BackgroundWorker
- イベントを使って、バックグラウンドとフォアグラウンドのスレッド間通信を実現する。
キャンセルの実験コード
using System.ComponentModel;
public class Program
{
private static BackgroundWorker backgroundWorker = new BackgroundWorker();
static void Main(string[] args)
{
Console.WriteLine("Press any key to cancel the operation...\n");
InitializeBackgroundWorker();
backgroundWorker.RunWorkerAsync();
// キー入力を待機し、入力があればキャンセル
Console.ReadKey(true);
Console.WriteLine("Key pressed. Cancelling...");
if (backgroundWorker.IsBusy)
{
Console.WriteLine("backgroundWorker.IsBusy is true. Cancelling...");
backgroundWorker.CancelAsync();
}
Console.WriteLine("Cancelling complete.");
// タスクが完了するのを待つ
while (backgroundWorker.IsBusy)
{
Console.WriteLine("backgroundWorker.IsBusy is true. Waiting...");
Thread.Sleep(100); // CPUの負荷を下げるために短い休止
}
Console.WriteLine("Main method complete. Press any key to exit.");
Console.ReadKey();
}
private static void InitializeBackgroundWorker()
{
backgroundWorker.WorkerSupportsCancellation = true;
backgroundWorker.DoWork += BackgroundWorker_DoWork;
backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
}
private static void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
int count = 0;
while (count < 50)
{
if (backgroundWorker.CancellationPending)
{
e.Cancel = true;
return;
}
Console.WriteLine($"Processing {count + 1}/50...");
Thread.Sleep(200); // 仮想的な作業時間
count++;
}
}
private static void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
Console.WriteLine("Operation was cancelled.");
}
else
{
Console.WriteLine("Operation completed successfully.");
}
}
}
出力(途中でaキーを押す)
Press any key to cancel the operation...
Processing 1/50...
Processing 2/50...
Processing 3/50...
Processing 4/50...
Processing 5/50...
Processing 6/50...
Processing 7/50...
Processing 8/50...
Processing 9/50...
Processing 10/50...
Key pressed. Cancelling... <-- ここでAキーを押す
backgroundWorker.IsBusy is true. Cancelling...
Cancelling complete.
backgroundWorker.IsBusy is true. Waiting...
backgroundWorker.IsBusy is true. Waiting...
Operation was cancelled.
Main method complete. Press any key to exit.
a <-- ここでAキーを押す
出力(タスク完了まで押さない)
Press any key to cancel the operation...
Processing 1/50...
Processing 2/50...
Processing 3/50...
Processing 4/50...
省略
Processing 49/50...
Processing 50/50...
Operation completed successfully.
Key pressed. Cancelling... <-- ここでAキーを押す
Cancelling complete.
Main method complete. Press any key to exit.
a <-- ここでAキーを押す
項目39 XAML環境でのスレッド間コールを理解しよう
ちょっと特殊そうなのでスキップ
項目40 同期をとるにはlock()を最初の選択肢にしよう
- Monitor.Enter, Exitに比べてlockの方が良い点
- lockでは、値型をオブジェクトとして設定しようとしたときにコンパイルエラーとなる。
- Monitorの場合は、ボックス化が行われるためコンパイルエラーにはならない。しかし、ボックス化の場合は同期オブジェクトが共通にならないため、排他制御ができない。また、Exitのときに同じまた別のボックスが生成されることになり、SynchronizationLockExceptionが発せられる。
- 詳細は実験コードに。
lockで同期を取る例
using System.Diagnostics;
public class Program
{
private int Count { get; set; }
object _lock = new ();
public static async Task Main()
{
var sw = new Stopwatch();
const int taskNum = 500000;
var tasks = new List<Task>(taskNum);
var program = new Program();
sw.Start();
for (var i = 0; i < taskNum; i++)
{
tasks.Add(Task.Run(() => program.Increment()));
}
await Task.WhenAll(tasks);
Console.WriteLine($"Count: {program.Count}, Elapsed: {sw.ElapsedMilliseconds}ms");
}
private void Increment()
{
lock (_lock)
{
Count++;
}
}
}
- 出力(lockの部分をコメントアウトした場合)
- 10回平均 182秒
Count: 434777, Elapsed: 150ms
- 出力(lockを有効にした場合)
- 10回平均 210秒
Count: 500000, Elapsed: 213ms
Monitor.Enter, Exitでもlockと同じく排他制御ができる
private void Increment()
{
// lock (_lock)
// {
// Count++;
// }
Monitor.Enter(_lock);
try
{
Count++;
}
finally
{
Monitor.Exit(_lock);
}
}
出力
Count: 500000, Elapsed: 237ms
- これではだめ1(値型を渡す)
private void Increment()
{
// 値型を渡しているが、コンパイルエラーにならない。ボックス化されるため。
Monitor.Enter(Count);
try
{
Count++;
}
finally
{
Monitor.Exit(Count);
}
}
出力
Unhandled exception. System.Threading.SynchronizationLockException: Object synchronization method was called from an unsynchronized block of code.
at System.Threading.Monitor.Exit(Object obj)
- これではだめ(値型を参照型に変換して渡す)
private void Increment()
{
// 個々の呼び出しがそれぞれ別のボックスを作成するため、ロックが効かない
object lockObj = Count;
Monitor.Enter(lockObj);
try
{
Count++;
}
finally
{
Monitor.Exit(lockObj);
}
}
出力
Count: 432748, Elapsed: 161ms
SharpLabで変換すると、Monitor.Enter, Exitが使われている
- 変換前
private void Increment()
{
lock (_lock)
{
Count++;
}
}
- 変換あと
private void Increment()
{
object @lock = _lock;
bool lockTaken = false;
try
{
Monitor.Enter(@lock, ref lockTaken);
Count++;
}
finally
{
if (lockTaken)
{
Monitor.Exit(@lock);
}
}
}
- lockには参照型のみを渡すことができる
- Interlockedクラスのメソッドを利用することでも同様の排他制御が実現できる
- 簡単なので、多くの単一演算の同期を取りたい場合にInterlockedクラスがその機能を提供していないか調べるとよい。
Interlocked.Incrementを利用する例
private void Increment()
{
// プロパティは引数に渡せない。フィールドはOK
Interlocked.Increment(ref _count);
}
出力
Count: 500000, Elapsed: 150ms
項目41 スコープが最小限のロックハンドルを使おう
- 同期プリミティブは、できるだけ局所化したい。同期プリミティブを使う場所がアプリケーションで多いほどデッドロックなどの問題に遭遇する(変数をprivateにしてスコープを小さくしたいのと同じ気持ち)
- ロックの対象を選ぶ時、呼び出し側から見えないprivateフィールドを選ぶのがよい。
-
lock(this)
とlock(typeof(MyType))
は、誰でもアクセスできる実態をベースとしてロックオブジェクトを作っているので、悪い方法である。- thisだと、自分から自分を指定する & 自分自身をインスタンスしたクラスも自分を指定する、というときに同じオブジェクトがロックの対象となる(これだけならOKな模様。デッドロックの発生シーンはまだつかめていない)
- ベストは、クラスにprivateなフィールドとしてobjectを作成し、それを同期オブジェクトとして使うこと。
ベストな同期ハンドルの持ち方
- 一番シンプルな実装
public class LockingExample
{
private object _syncHandle = new object();
public void IncrementCount()
{
lock (_syncHandle)
{
// Increment the count
}
}
}
- ただし、同期ハンドルのコピーが何かの運の悪いタイミングで作成されないように、InterlockedクラスのCompareExchangeメソッドを使うとさらによい。これを行うと型に必ず1個しか同期ハンドルが作られないことを担保できる。
public class LockingExample
{
private object _syncHandle;
private object GetSyncHandle()
{
Interlocked.CompareExchange(ref _syncHandle, new object(), null);
return _syncHandle;
}
public void IncrementCount()
{
lock (GetSyncHandle())
{
// Increment the count
}
}
}
MethodImplで同期を取る方法
- 項目40の例に引き続き、以下の方法でも同期を取ることができる。
- ただし、メソッド自体をロックすることにより、本当に必要な箇所だけではなくなるため、パフォーマンス的には劣る。
[MethodImpl(MethodImplOptions.Synchronized)]
private void Increment()
{
_count++;
}
出力
Count: 500000, Elapsed: 215ms
項目42 ロックしたセクションで未知のコードを呼び出さない
- イベントのInvokeメソッドのように、他のクラスから突っ込まれて具体的にどのような処理があるかわからないようなコードは、lockブロックの中で呼ばないべきである。
- 未知のコードへの呼び出しパターン
- イベントの発行
- 引数として渡されたデリゲートやラムダ式を呼び出すこと
- 仮想メソッド
このスクラップは2024/04/27にクローズされました