非同期処理で書き込みと読み込みを同時にしたらどうなるか?では、同時に同じファイルに書き込もうとした場合や書き込みをしている最中に読み込みを開始した場合は例外が発生すると書いています。では書き込みと読み込みがそれぞれどのタイミングでおこなわれるかコントロールできない状態ではどうすればよいのでしょうか?
同時に同じファイルに書き込もうとすると例外が発生する
短い文字列だと書き込みはすぐ終わるので0.1秒ずつ待機しながら書き込むSaveFileメソッドを定義し、これをawaitしないで2回実行してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
public partial class Form1 : Form { // 短い文字列だと書き込みはすぐ終わるので0.1秒ずつ待機しながら書き込む void SaveFile(int id) { Task.Run(() => { try { StreamWriter sw = new StreamWriter(@".\file.txt"); for (int i = 0; i < 10; i++) { sw.WriteLine(i.ToString()); Console.WriteLine($"引数:{id} \"{i}\"を書き込んでいます"); Thread.Sleep(100); } sw.Close(); } catch { Console.WriteLine($"例外:引数{id}の書き込みに失敗しました"); return; } Console.WriteLine($"引数{id}の書き込み終了"); }); } private void button2_Click(object sender, EventArgs e) { SaveFile(1); SaveFile(2); } } |
ひとつめの書き込みをおこなっているときに二つ目の書き込みの処理がおこなわれようとしたので例外が発生してしまいました(ひとつめの書き込みは最後までおこなわれる)。
|
引数:1 "0"を書き込んでいます 例外がスローされました: 'System.IO.IOException' (mscorlib.dll の中) 例外:引数2の書き込みに失敗しました 引数:1 "1"を書き込んでいます 引数:1 "2"を書き込んでいます 引数:1 "3"を書き込んでいます 引数:1 "4"を書き込んでいます 引数:1 "5"を書き込んでいます 引数:1 "6"を書き込んでいます 引数:1 "7"を書き込んでいます 引数:1 "8"を書き込んでいます 引数:1 "9"を書き込んでいます 引数1の書き込み終了 |
排他ロックで例外を回避する
そこでlock文をつかって定義しなおします。上記の例では同期オブジェクトとしてリストを指定しましたが、同期オブジェクトは何でもかまいません。
クリティカルセクションに入る前に、同期オブジェクトをロックします。ロックされている間、他のスレッドは同じオブジェクトをロックすることができず、アンロックされるまで待たされます。そしてロックしたスレッドはクリティカルセクションを終えた後にアンロックします。
ひとつめの書き込みをおこなっているときに二つ目の書き込みの処理がおこなわれようとした場合は終了するまで待たされます。そしてひとつめの書き込みが終了してからふたつめの書き込みが開始されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
public partial class Form1 : Form { object syncObj = new object(); // 同期オブジェクト void SaveFile(int id) { Task.Run(() => { try { lock (syncObj) { StreamWriter sw = new StreamWriter(@".\file.txt"); for (int i = 0; i < 10; i++) { sw.WriteLine(i.ToString()); Console.WriteLine($"引数:{id} \"{i}\"を書き込んでいます"); Thread.Sleep(100); } sw.Close(); } } catch { Console.WriteLine($"例外:引数{id}の書き込みに失敗しました"); return; } Console.WriteLine($"引数{id}の書き込み終了"); }); } } |
実行結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
引数:1 "0"を書き込んでいます 引数:1 "1"を書き込んでいます 引数:1 "2"を書き込んでいます 引数:1 "3"を書き込んでいます 引数:1 "4"を書き込んでいます 引数:1 "5"を書き込んでいます 引数:1 "6"を書き込んでいます 引数:1 "7"を書き込んでいます 引数:1 "8"を書き込んでいます 引数:1 "9"を書き込んでいます 引数1の書き込み終了 引数:2 "0"を書き込んでいます 引数:2 "1"を書き込んでいます 引数:2 "2"を書き込んでいます 引数:2 "3"を書き込んでいます 引数:2 "4"を書き込んでいます 引数:2 "5"を書き込んでいます 引数:2 "6"を書き込んでいます 引数:2 "7"を書き込んでいます 引数:2 "8"を書き込んでいます 引数:2 "9"を書き込んでいます 引数2の書き込み終了 |
書き込みと読み込みのタイミングが同じになりそうな場合も同じ方法で対応できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
public partial class Form1 : Form { void ReadFile(int i) { Task.Run(() => { try { lock (syncObj) { StreamReader sr = new StreamReader(@".\file.txt"); List<string> vs = new List<string>(); while (true) { string str = sr.ReadLine(); if (str == null) break; vs.Add(str); Thread.Sleep(100); } sr.Close(); } } catch (Exception ex) { Console.WriteLine("例外発生"); } }); } private void button2_Click(object sender, EventArgs e) { SaveFile(1); Thread.Sleep(500); ReadFile(2); // 書き込みの最中に読み込みをおこなおうとしても例外は発生しない // 書き込みが終了するまで待たされる } } |
ロック中にawaitするのであればSemaphoreSlim
しかしlock文にも短所があります。ロックしているときにawaitが使えないことです。ロックはしたい、けれどもawaitもしたいというのであればlock文は使えません。そんなときはSemaphoreSlimを使うのがおすすめです。こちらはawaitが使えます。
以下のコードは同じファイルへの読み出しと書き込みが同時におきないように制御するコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
public partial class Form1 : Form { SemaphoreSlim sem = new SemaphoreSlim(1, 1); void SaveFile2(int id) { Task.Run(async () => { await sem.WaitAsync().ConfigureAwait(false); try { StreamWriter sw = new StreamWriter(@".\file.txt"); for (int i = 0; i < 10; i++) { sw.WriteLine(i.ToString()); Console.WriteLine($"引数:{id} \"{i}\"を書き込んでいます"); await Task.Delay(100); } sw.Close(); Console.WriteLine($"引数{id}の書き込み終了"); } catch { Console.WriteLine($"例外:引数{id}の書き込みに失敗しました"); return; } finally { sem.Release(); } }); } void ReadFile2(int i) { Task.Run(async () => { try { await sem.WaitAsync().ConfigureAwait(false); StreamReader sr = new StreamReader(@".\file.txt"); List<string> vs = new List<string>(); while (true) { string str = sr.ReadLine(); if (str == null) break; vs.Add(str); Console.WriteLine($"引数:{i} \"{str}\"を読み込みました"); await Task.Delay(100); } sr.Close(); Console.WriteLine($"引数:{i} すべて読み込みました"); } catch (Exception ex) { Console.WriteLine($"例外:引数:{i}の読み込みに失敗しました"); Console.WriteLine(ex.Message); } finally { sem.Release(); } }); } private void button2_Click(object sender, EventArgs e) { SaveFile2(1); ReadFile2(2); SaveFile2(3); } } |
この場合も同時に複数の書き込みと読み出しが行なわれないように制御することができます。
鳩でも分かるC#管理人からのお願い
できる仕事であれば請け負います。鳩でもわかるC#管理人はクラウドワークスに在宅ワーカーとして登録しています。お仕事の依頼もお待ちしております。
⇒ 仕事を依頼する
コメントについて
コメントで英語などの外国語でコメントをされる方がいますが、管理人は日本語以外はわからないので基本的に内容が理解できず、承認することもありません。それからへんな薬を売っているサイトやリンク先のサイトが存在しないというスパムコメントも多々あります。
Some people make comments in foreign languages such as English, but since the manager does not understand anything other than Japanese, he basically cannot understand the content and does not approve it. Please use Japanese when making comments.
そんななか日本語のコメントもいただけるようになりました。「○○という変数はどこで宣言されているのか?」「××というメソッドはどこにあるのか」「例外が発生する」「いっそのことソース丸ごとくれ」という質問ですが、管理人としては嬉しく思います。「自分が書いた記事は読まれているんだな」と。疑問点には可能な限り答えます。記事に問題があれば修正いたします。
そのうえでお願いがあります。「匿名」という味も素っ気もない名前ではなく、捨てハンでいいのでなにかハンドルネームをつくってほしいと思います。
管理人のモチベーションアップのために
よろしければご支援お願いします。
⇒ 管理人の物乞いリスト