非同期処理で書き込みと読み込みを同時にしたらどうなるか?では、同時に同じファイルに書き込もうとした場合や書き込みをしている最中に読み込みを開始した場合は例外が発生すると書いています。では書き込みと読み込みがそれぞれどのタイミングでおこなわれるかコントロールできない状態ではどうすればよいのでしょうか?
同時に同じファイルに書き込もうとすると例外が発生する
短い文字列だと書き込みはすぐ終わるので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 2 3 4 5 6 7 8 9 10 11 12 13 |
引数: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); } } |
この場合も同時に複数の書き込みと読み出しが行なわれないように制御することができます。