MochiuWiki : SUSE, EC, PCB
案内
メインページ
最近の更新
おまかせ表示
MediaWiki についてのヘルプ
ツール
リンク元
関連ページの更新状況
特別ページ
ページ情報
We ask for
Donations
検索
個人用ツール
ログイン
Toggle dark mode
名前空間
ページ
議論
表示
閲覧
ソースを閲覧
履歴を表示
C Sharpの基礎 - マルチスレッドのソースを表示
提供: MochiuWiki : SUSE, EC, PCB
←
C Sharpの基礎 - マルチスレッド
あなたには「このページの編集」を行う権限がありません。理由は以下の通りです:
この操作は、次のグループのいずれかに属する利用者のみが実行できます:
管理者
、new-group。
このページのソースの閲覧やコピーができます。
== 概要 == C#のマルチスレッドプログラミングは、並行処理を実現する強力な機能である。<br> .NET Framework 4.5以降、C# 5.0以降はコンパイラレベルでマルチスレッドをサポートしており、特に<code>async</code> / <code>await</code>キーワードの導入により、非同期プログラミングが大幅に簡素化された。<br> <br> タスクベース非同期パターン (Task-based Asynchronous Pattern: TAP) は、この新しいアプローチの中心となる概念である。<br> TAPでは、非同期メソッドの名前に<code>Async</code>というサフィックスを付けて、戻り値の型として<code>Task</code>、<code>Task<T></code>、<code>void</code>を使用する。<br> <br> <code>async</code>キーワードは、メソッドが非同期であることを示す。<br> このキーワードを使用することにより、メソッド内で<code>await</code>キーワードを使用できるようになる。<br> <code>await</code>は、非同期操作の完了を待機するために使用され、その間にスレッドをブロックすることなく、制御を呼び出し元に返す。<br> <br> 以下の例では、MethodAsync1とMethodAsync2という2つの非同期メソッドが定義されている。<br> これらのメソッドは、それぞれTask<int>型とTask<(bool, string)>型 (タプル型) を返す。<br> <br> これらのメソッド内では、Task.Runを使用して新しいタスクを開始し、Task.Delayで非同期の待機を模倣している。<br> 実際のアプリケーションでは、この部分が時間のかかる操作 (ファイルI/Oやネットワーク通信等) に置き換わることが多い。<br> <br> <syntaxhighlight lang="c#"> // Task<int>型を返す非同期関数 public async Task<int> MethodAsync1() { var iRet = await Task.Run(() => { await Task.Delay(5000); return 0; }); return iRet; } // Task<(bool, string)>型 (タプル型) を返す非同期関数 public async Task<(bool, string)> MethodAsync2() { var tRet = await Task.Run(() => { await Task.Delay(5000); return (true, @"some strings");; }); return tRet; } </syntaxhighlight> <br> C#のマルチスレッドプログラミングのメリットは多岐にわたる。<br> UIの応答性の向上、複数のCPUコアを効率的に利用、I/O操作を最適化すること等が可能である。<br> <br> しかし、マルチスレッドプログラミングには注意点もある。<br> 例えば、デッドロックやレースコンディション等の同期の問題に注意する必要がある。<br> また、過度にスレッドを使用する場合、却ってパフォーマンスが低下する可能性もある。<br> <br> C#は、これらの課題に対処するための様々なツールを提供している。<br> 例えば、<code>lock</code>キーワード、<code>Monitor</code>クラス、<code>Interlocked</code>クラス等が存在する。<br> また、より高度な並行処理のためには、Task Parallel Library (TPL)やParallel LINQ (PLINQ)等のライブラリも用意されている。<br> <br> マルチスレッドプログラミングは強力であるが、適切に使用することが重要である。<br> シンプルな操作には同期プログラミング、複雑な非同期操作には<code>async</code> / <code>await</code>パターンを使用する等、状況に応じて適切なアプローチを選択することが、<br> 効率的で保守しやすいコードを記述するための鍵となる。<br> <br><br> == asyncとawaitの動作 == タスクとは、一連の処理をひとまとまりにした単位である。<br> <code>Task</code>クラスはマルチスレッドで実行されるようになっており、連続で記述する場合は、記述した個数だけマルチスレッドで並列処理される。<br> <br> <code>Task.Run</code>メソッドは、大量の計算のみを行う処理を並列実行させる時のみに使用する場合、または、既存のメソッドを変更せずに使用する場合に使用する。<br> <syntaxhighlight lang="c#"> public async Task MethodAsync() { var task1 = Task.Run(() => { // 何か処理1 }); var task2 = Task.Run(() => { // 何か処理2 }); var task12 = Task.WhenAll(task1, task2); var taskAll = task12.ContinueWith(() => { // 何か処理3 }); return await taskAll.ConfigureAwait(false); } </syntaxhighlight> <br> <code>Task.Run</code>メソッドには、引数を渡すオーバーロードが存在しない。<br> 引数を渡す場合、<code>Task</code>クラスのインスタンスの生成と同時に引数を渡して、<code>Task</code>クラスの<code>Start</code>メソッドを使用してスレッドを実行する必要がある。<br> <syntaxhighlight lang="c""> private async void MethodAsync() { int m = 999; Task<int> task = new Task(x => { Thread.Sleep(3000); return x * 2; }, m); task.Start(); m = 100; int result = await task; // result : 1998 Console.WriteLine($"{result}"); } </syntaxhighlight> <br><br> == Wait / Result == コンソールソフトウェアにおいて、<code>Wait</code>メソッドおよび<code>Result</code>メソッドを使用して、非同期メソッドを作成することができる。<br> <syntaxhighlight lang="c#"> HttpClient hc = new HttpClient(); string html = hc.GetStringAsync("http://example.jp/").Result; </syntaxhighlight> <br> また、<code>Wait</code>メソッドおよび<code>Result</code>メソッド以外に、<code>WaitAll</code>メソッドや<code>WaitAny</code>メソッドが存在する。<br> <syntaxhighlight lang="c#"> HttpClient hc = new HttpClient(); Task<string> t1 = hc.GetStringAsync("https://www.microsoft.com/"); Task<string> t2 = hc.GetStringAsync("https://www.bing.com/"); // タスクが終わるまでスレッドをブロック t1.Wait(); // タスクが終わるまでスレッドをブロックして結果を取得 string binghtml = t2.Result; // どれかのタスクが終わるまでスレッドをブロック int completedTaskIndex = Task.WaitAny(t1, t2); // 0 : タスクt1の方が速い場合 // 1 : タスクt2の方が速い場合 // どれかのタスクが終わるまでスレッドをブロック(タイムアウトあり) int completedTaskIndex2 = Task.WaitAny(new[] { t1, t2 }, 50); // 0 : タスクt1の方が速い場合 // 1 : タスクt2の方が速い場合 // -1 : タスクt1およびt2とも50[ms]以内に応答がない場合 // 全てタスクが終わるまでスレッドをブロック Task.WaitAll(t1, t2); // 全てのタスクが終わるまでスレッドをブロック(タイムアウトあり) bool allTasksCompleted = Task.WaitAll(new[] { t1, t2 }, 50); // true : タスクt1およびt2とも50[ms]以内に応答がある場合 // false : タスクt1またはt2のいずれか一方、または、タスクt1およびt2とも50[ms]以内に応答がない場合 </syntaxhighlight> <br><br> == WhenAny == 複数のタスクのうち、いずれか1つのみが完了するまで待機する場合、<code>WhenAny</code>メソッドを使用する。<br> <syntaxhighlight lang="c#"> HttpClient hc = new HttpClient(); Task<string> t1 = hc.GetStringAsync("https://www.microsoft.com/"); Task<string> t2 = hc.GetStringAsync("https://www.bing.com/"); Task<string> completedTask = await Task.WhenAny(t1, t2); </syntaxhighlight> <br> これを応用して、処理継続中のタスクをキャンセルすることもできる。<br> <syntaxhighlight lang="c#"> HttpClient hc = new HttpClient(); CancellationTokenSource cts = new CancellationTokenSource(); // GetStringAsyncメソッドにはCancellationToken構造体を受けるオーバーロードが無いため、GetAsyncメソッドを使用している Task<HttpResponseMessage> t1 = hc.GetAsync("https://www.microsoft.com/", cts.Token); Task<HttpResponseMessage> t2 = hc.GetAsync("https://www.bing.com/", cts.Token); // 完了したタスクを取得 Task<HttpResponseMessage> completedTask = await Task.WhenAny(t1, t2); // 他の処理中のタスクは全てキャンセルする cts.Cancel(); // 完了したタスクの結果を取得 HttpResponseMessage msg = await completedTask; string html = await msg.Content.ReadAsStringAsync(); </syntaxhighlight> <br><br> == WhenAllメソッド / ContinueWithメソッド == <code>Task</code>クラスは<code>ContinueWith</code>メソッドを使用して、その後の処理を連続して実行できる。<br> <br> 例えば、2つ以上のタスクを実行して全てのタスクが完了した後に続けてタスクを実行する場合、<code>ContinueWith</code>メソッドまたは<code>await</code>を使用してタスクの完了を待つ。<br> <syntaxhighlight lang="c#"> // パターン1 var task1 = Task.Run(() => { // 何か処理 }); var task2 = Task.Run(() => { // 何か処理 }); var task3 = Task.Run(() => { // 何か処理 }); var taskAll = Task.WhenAll(task1, task2).ContinueWith(task3); // awaitは不要 </syntaxhighlight> <br> <syntaxhighlight lang="c#"> // パターン2 var task1 = Task.Run(() => { // 何か処理 }); await task1.ConfigureAwait(false); var task2 = Task.Run(() => { // 何か処理 }); await task2.ConfigureAwait(false); var task3 = Task.Run(() => { // 何か処理 }); await task3.ConfigureAwait(false); </syntaxhighlight> <br> 状況にもよるが、処理が膨大になると可読性が落ちるため、先にタスクをまとめて記述して、タスク間の関係性が見通せるパターン1の方が良い。<br> <br><br> == IsCompletedプロパティ == タスクが完了しているかどうかを確認する場合、<code>IsCompleted</code>プロパティを使用する。<br> <br> <code>IsCompletedSuccessfully</code>プロパティもあるが、これはタスクが成功および完了した場合のみ<code>true</code>になる。<br> また、<code>IsFaulted</code>プロパティや<code>IsCanceled</code>プロパティも存在する。<br> <syntaxhighlight lang="c#"> HttpClient hc = new HttpClient(); Task<string> task = hc.GetStringAsync("https://www.microsoft.com/"); // タスクが成功および完了したかどうかを確認 if (!task.IsCompleted) { // この時点でタスクt1が成功および完了している場合、このメッセージは表示されない Console.WriteLine("ちょっとまってね"); } var html = await task; Console.WriteLine(html); </syntaxhighlight> <br><br> == ConfigureAwait(false) == GUIアプリケーションにおいて、<code>Wait</code>メソッドおよび<code>Result</code>メソッドを使用する場合、デッドロックが発生する。<br> これは、<code>Task</code>クラスの<code>Wait</code>メソッドや<code>Result</code>メソッドは、元のスレッドへ戻るからである。<br> <br> 以下の例では、まず、methodAsync().Wait()を実行した時点で親スレッドがスリープする。<br> 次に、methodAsyncメソッドの完了後、親スレッドに戻る時、既に親スレッドがスリープしているため、処理を続けることができない。<br> そのため、親スレッドは子スレッドを待ち続け、子スレッドは親スレッドに戻ろうとして、デッドロックが発生する。<br> <syntaxhighlight lang="c#"> // デッドロックが起きるサンプルコード(GUIアプリケーションのみ) // コンソールアプリケーションでは正常に動作する public void callMethod() { methodAsync().Wait(); } public async Task methodAsync() { return await Task.Delay(1000); } </syntaxhighlight> <br> 対策として、<code>ConfigureAwait(false)</code>を使用するとデッドロックが回避できる。<br> <br> <code>ConfigureAwait(false)</code>は、メソッドの終了後に親スレッド(呼び出し元)に戻らなくてもよいことを指定する。<br> ブロックされたスレッドに戻らなくなるため、methodAsyncメソッドが終了した時にcallMethodメソッドに戻ることができる。<br> <syntaxhighlight lang="c#"> // デッドロックが起きないサンプルコード(GUIアプリケーションのみ) public void callMethod() { methodAsync().Wait(); } public async Task methodAsync() { return await Task.Delay(1000).ConfigureAwait(false); } </syntaxhighlight> <br><br> == Parallelクラス == CPU処理を並列実行する場合、<code>Parallel</code>クラスを使用する。<br> <br> 以下の例では、methodAsync1メソッド、methodAsync2メソッド、methodAsync3メソッドが並列実行される。<br> <u>ただし、CPUのコア数やスレッドプールの空き状況によっては、並列実行されない場合もあることに注意する。</u><br> <br> なお、<code>Parallel</code>クラスには、<code>Parallel.Invoke</code>メソッドの他に<code>Parallel.For</code>メソッドや<code>Parallel.ForEach</code>メソッドも存在する。<br> また、<code>Parallel.Invoke</code>メソッドの第1引数の<code>ParallelOptions</code>クラスは省略可能である。<br> <syntaxhighlight lang="c#"> // Invokeメソッドの引数はAction型 Parallel.Invoke(new ParallelOptions() { MaxDegreeOfParallelism = 4 }, // 最大同時並列数:4 methodAsync1, // 戻り値が無いAction型の場合 () => { methodAsync2(100); }, // 戻り値があるFunc型の場合 () => { // 直接処理を記述してもよい int n = 400; methodAsync3(n); } ); </syntaxhighlight> <br><br> == PLINQ(ParallelEnumerable) == <code>LINQ</code>(<code>PLINQ</code>)を使用して、並列実行することもできる。<br> <code>PLINQ</code>では、<code>AsParallel</code>拡張メソッド以降のコードが並列化される。<br> <br> 以下の例では、処理A(ファイルの読み込み)はシングルスレッドで実行、処理B(ハッシュ値計算)はマルチスレッドで実行している。<br> なお、<code>AsParallel</code>拡張メソッドの実行後の<code>WithDegreeOfParallelism</code>拡張メソッドは省略可能である。<br> <syntaxhighlight lang="c#"> // ファイルのハッシュ値を計算するプログラム var files = Directory.GetFiles(Environment.SystemDirectory, "*.exe"); var filehash = files .Select(f => new { File = f, Data = File.ReadAllBytes(f) }) // 処理A(ファイルの読み込み) .AsParallel() .WithDegreeOfParallelism(4) // 最大同時並列数:4 .Select(f => new { File = f.File, Hash = SHA256.Create().ComputeHash(f.Data)}) // 処理B(ハッシュ値計算) .ToArray(); </syntaxhighlight> <br><br> == 排他処理 == 排他処理を行う場合、<code>lock</code>キーワードが基本的かつ一般的な方法である。<br> しかし、状況や要件によっては他の手法も考慮する。<br> <br> <code>lock</code>キーワード以外にも以下に示すような選択肢がある。<br> * Monitorクラス *: lockキーワードは内部的に<code>Monitor</code>クラスを使用している。 *: <code>Monitor</code>クラスを直接使用する場合、より細かい制御が可能になる。 *: <br> * Mutexクラス *: プロセス間の同期に使用する。 *: 複数のアプリケーションインスタンス間での排他制御が必要な場合に適している。 *: <br> * Semaphore / SemaphoreSlim *: リソースへのアクセスを制限する必要がある場合に使用する。 *: 例えば、同時に最大N個のスレッドがリソースにアクセスできるようにする場合等である。 *: <br> * ReaderWriterLockSlim *: 複数の読み取りスレッドを同時に許可しながら、書き込みを排他的に行うことができる。 *: <br> *: 読み取り操作が書き込み操作よりも頻繁に行われる場合に適している。 *: また、ファイル操作に要する時間が長くなり、同時読み取りの利点が大きくなる場合にも適している。 *: その他、アプリケーションの規模が大きくなり、より細かい制御が必要になった場合等にもよい。 *: <br> *: ただし、小規模なファイル操作の場合は、ReaderWriterLockSlimのオーバーヘッドがlockキーワードよりも大きくなる可能性があることに注意する。 *: <br> * Interlockedクラス *: 単一の変数に対する原子的操作 (インクリメント、デクリメント、交換等) を提供する。 *: <br> * SpinLock構造体 *: 非常に短い時間だけロックが必要な場合に使用する。 *: CPUリソースを多く消費するが、コンテキストスイッチのオーバーヘッドを避けることができる。 <br> * 選択の基準 ** 同期の範囲 **: 単一のアプリケーション内か、プロセス間か ** 操作の粒度 **: 細かい操作か、大きなブロックか ** パフォーマンス要件 **: 高頻度の短い操作か、低頻度の長い操作か ** 読み書きのバランス **: 読み取りと書き込みの頻度のバランス <br> ==== lockキーワードの使用 ==== lockキーワードのメリットと特徴を以下に示す。<br> * 使いやすさ *: シンプルで直感的な構文を持つ。 * 安全性 *: デッドロックのリスクを軽減する。(ただし、完全に防ぐわけではない) * パフォーマンス *: 比較的軽量な操作である。 <br> 以下の例では、排他処理 (<code>lock</code>キーワード) を使用してテキストファイルを2つのスレッドから読み書きしている。<br> <syntaxhighlight lang="c#"> using System; using System.IO; using System.Text; using System.Threading.Tasks; class Program { private static readonly object fileLock = new object(); private const string fileName = "sample.txt"; static async Task Main(string[] args) { try { Task readTask = ReadFileAsync(); Task writeTask = WriteFileAsync(); await Task.WhenAll(readTask, writeTask); } catch (Exception ex) { Console.WriteLine($"メインプロセスでエラーが発生: {ex.Message}"); } finally { Console.WriteLine("プログラムの終了"); } } static async Task ReadFileAsync() { for (var i = 0; i < 10; i++) { try { string content = await ReadFromFileAsync(); Console.WriteLine($"読み込んだ内容: {content}"); } catch (Exception ex) { Console.WriteLine($"読み込み中にエラーが発生: {ex.Message}"); } await Task.Delay(1000); } } static async Task WriteFileAsync() { int counter = 0; for (var i = 0; i < 10; i++) { try { string content = $"カウンター: {counter++}"; await WriteToFileAsync(content); Console.WriteLine($"書き込んだ内容: {content}"); } catch (Exception ex) { Console.WriteLine($"書き込み中にエラーが発生: {ex.Message}"); } await Task.Delay(1500); } } static async Task<string> ReadFromFileAsync() { if (!File.Exists(fileName)) { throw new FileNotFoundException($"ファイルが見つかりません: {fileName}"); } try { lock (fileLock) { using (StreamReader reader = new StreamReader(fileName)) { return reader.ReadToEnd(); } } } catch (IOException ex) { throw new IOException($"ファイルの読み込み中にエラーが発生: {ex.Message}", ex); } } static async Task WriteToFileAsync(string content) { try { lock (fileLock) { using (StreamWriter writer = new StreamWriter(fileName, false)) { writer.Write(content); } } } catch (IOException ex) { throw new IOException($"ファイルの書き込み中にエラーが発生: {ex.Message}", ex); } } } </syntaxhighlight> <br><br> == 例外処理 == <code>Task</code>クラスや<code>Parallel</code>クラスで発生する例外は、例外<code>AggregateException</code>としてキャッチすることができる。<br> <syntaxhighlight lang="c#"> try { Parallel.Invoke( () => throw new ArgumentException(), () => throw new InvalidOperationException(), () => throw new FormatException() ); } catch (AggregateException exception) { // 原因となるExceptionを確認する場合、FlattenメソッドのInnerExceptionsプロパティを使用する var exceptions = exception.Flatten().InnerExceptions; foreach (var ex in exceptions) { Debug.WriteLine(ex.GetType()); } } catch (Exception ex) { // 未処理の例外をキャッチ Trace.WriteLine($"Log: {ex}"); } catch { // SEHをキャッチ Trace.WriteLine("Log: Caught uninterpreted exception."); } </syntaxhighlight> <br> ただし、<code>Task</code>クラスを<code>await</code>する場合、個別の<code>Exception</code>クラスを使用してキャッチすることもできる。<br> <syntaxhighlight lang="c#"> try { var addresses = await Dns.GetHostAddressesAsync("example.jp"); } catch(SocketException exception) { Debug.WriteLine(exception.ToString()); } catch (Exception ex) { // 未処理の例外をキャッチ Trace.WriteLine($"Log: {ex}"); } catch { // SEHをキャッチ Trace.WriteLine("Log: Caught uninterpreted exception."); } </syntaxhighlight> <br> <code>Task</code>クラスで発生したキャッチされていない例外をまとめて処理する場合、<code>TaskScheduler.UnobservedTaskException</code>メソッドを使用する。<br> <u>ただし、これは、例外が発生したタイミングでキャッチできないこと、および、例外をキャッチできたとしても対処可能なことが限られることに注意する。</u><br> <syntaxhighlight lang="cpp"> private async void buttonl_Click(object sender, RoutedEventArgs e) { await Task.Run(() => { await Task.Delay(1000); }); // 非同期処理の未処理例外をキャッチする TaskScheduler.UnobservedTaskException += (sender, e) => { AggregateException ae = e.Exception; Debug.WriteLine(ae.ToString()); e.SetObserved(); // .NET Framework 4.0の場合、これを記述しないとソフトウェアが強制終了する }; } </syntaxhighlight> <br> 上記の例外処理において、以下のようなメソッドを記述すると便利である。<br> <code>ValueTask</code>構造体ではなく<code>Task</code>クラスでもよいが、<code>ValueTask</code>構造体にすることにより、アロケーションコストを削減できる可能性がある。<br> <syntaxhighlight lang="c#"> public void Button_Click(object sender, EventArgs e) { SafeAsyncBlock(async () => { // 非同期で取得 var body = await httpClient.GetStringAsync("https://example.com/foobar"); var foobar = JsonConvert.DeserializeObject<FooBar>(body); // 表示 textBox.Text = $"${foobar.Price}"; }); } // 非同期処理で発生する例外を安全に処理する public static async void SafeAsyncBlock(Func<ValueTask> action) { try { // 非同期処理を実行 await action(); } catch (Exception ex) { // 未処理の例外を補足する Trace.WriteLine($"Log: {ex}"); } catch { // SEHも補足 Trace.WriteLine("Log: Caught uninterpreted exception."); } } </syntaxhighlight> <br> <u>※注意</u><br> <u>普段は考慮する必要はないと思われるが、SEHを非同期処理の例外として伝搬して継続させることはできない。</u><br> <u>したがって、<code>catch</code>句で補足できるSEHは、最初のタスクコンテキストスイッチが発生する以前のSEHのみとなる。</u><br> <br> <u>1度タスクコンテキストスイッチが発生した後の非同期継続処理でSEHが発生した場合は、この方法では対処できない。</u><br> <u>おそらく、<code>ThreadPool</code>クラスから割り当てられたワーカースレッドの根元に伝搬する、または、<code>SynchronizationContext</code>がホストするスレッドの根元に伝搬する。</u><br> <br> <u><code>AppDomain</code>でフックする以外に良い方法は存在しないかもしれないことに注意する。</u><br> <br><br> __FORCETOC__ [[カテゴリ:C_Sharp]]
C Sharpの基礎 - マルチスレッド
に戻る。
案内
メインページ
最近の更新
おまかせ表示
MediaWiki についてのヘルプ
ツール
リンク元
関連ページの更新状況
特別ページ
ページ情報
We ask for
Donations
Collapse