C Sharpの基礎 - 排他制御
概要
排他制御は、複数のスレッドやプロセスが共有リソースに同時にアクセスすることによって発生する競合状態を防ぐことが主な目的である。
C#では、以下に示すような排他制御メカニズムが提供されている。
- lockステートメント
- 最も簡単な同期プリミティブであり、モニタロックを取得するためのシンタックスを提供する。
- 内部的には、Monitorクラスを使用している。
private static readonly object _lock = new object(); public void SafeMethod() { lock (_lock) { // 排他的にアクセスする処理 // ...略 } }
- Monitorクラスの直接的な使用
- 高度な制御が必要な場合、Monitorクラスを直接的に使用することにより、タイムアウトの設定やロックの状態確認等が可能になる。
- SemaphoreSlim
- 非同期処理における排他制御として、同一のプロセスの場合はSemaphoreSlimを使用することが推奨される。
- 軽量で高性能な同期プリミティブであり、async / awaitパターンとの相性が良い。
- MutexおよびSemaphore
- プロセス間同期が必要な場合は、MutexやSemaphoreを使用する。
- これは、OSレベルの同期機構を提供しており、異なるプロセス間での調整が可能である。
- ただし、システムリソースを消費するため、同一プロセス内での使用はlockステートメントやSemaphoreSlimが推奨される。
- ReaderWriterLockSlim
- 読み書き問題に対応するためのReaderWriterLockSlimも提供されている。
- これは読み取り操作が多く、書き込み操作が少ない状況で特に有効となる。
- 複数の読み取りスレッドを同時に許可しながら、書き込み時の排他制御を実現することができる。
さらに重要な概念として、インターロック操作がある。
Interlockedクラスは、アトミックな操作を提供しており、カウンタのインクリメントやデクリメント等の単純な操作を安全に行うことができる。
排他制御を実装するにあたり、デッドロックの防止が挙げられる。
複数のロックを取得する必要がある場合は、常に同じ順序でロックを取得する等、一貫性のあるアプローチが必要である。
また、パフォーマンスにおいては、ロックの範囲は必要最小限に抑えることが重要である。
長時間のロック保持は、並行処理の利点を失わせ、アプリケーション全体のスケーラビリティを低下させる可能性がある。
細かい粒度での制御が必要な場合、volatileキーワードやメモリバリアを使用することにより、低レベルでの同期制御も可能であるが、使用には深い知識が必要となる。
SemaphoreSlim
最も軽量で、軽量で、非同期操作に最適化されたセマフォを使用する。
メモリ使用量が少なく、パフォーマンスが高い。
ただし、同一プロセス内での排他制御では推奨されるが、異なるプロセス間での排他制御には適していない。
// 軽量で、非同期操作に最適化されたセマフォを使用
private static readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
public static async Task AccessFileAsync()
{
// セマフォの取得を待機
try
{
await semaphore.WaitAsync();
try
{
// ファイルの読み書き
await File.WriteAllTextAsync("sample.txt", "データ" + DateTime.Now);
string content = await File.ReadAllTextAsync("sample.txt");
Console.WriteLine($"読み取り結果 : {content}");
}
catch (IOException ex)
{
Console.WriteLine($"ファイルアクセスエラー : {ex.Message}");
throw; // 上位層で処理するように再スロー
}
catch (UnauthorizedAccessException ex)
{ // アクセス権限に関する例外処理
Console.WriteLine($"アクセス権限エラー : {ex.Message}");
throw;
}
finally
{
semaphore.Release();
}
}
catch (OperationCanceledException)
{ // WaitAsyncメソッドがキャンセルされた場合の処理
Console.WriteLine("セマフォの取得がキャンセルされました");
throw;
}
}
// 使用例
try
{
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
tasks.Add(AccessFileAsync());
}
await Task.WhenAll(tasks);
tasks.Clear();
}
catch (Exception ex)
{
Console.WriteLine($"実行中にエラーが発生 : {ex.Message}");
}
Mutex
異なるプロセス間での排他制御が可能である。
システム全体でのグローバルな名前を持つことができる。
タイムアウト機能を実装しやすい。
ただし、SemaphoreSlimと比較してオーバーヘッドが大きいことに注意する。
※注意
重層的な例外処理が必要となる。
- Mutexの作成時の例外
- WaitOneメソッドでのAbandonedMutexException例外
- ReleaseMutexでの例外
- ファイルアクセスの例外
// プロセス間での排他制御が必要な場合に使用
private const string MutexName = "GlobalFileMutex";
public static async Task AccessFileAsync()
{
Mutex mutex = null;
try
{
mutex = new Mutex(false, MutexName);
// Mutexの取得を待機 (タイムアウトを30秒とする)
bool hasMutex = false;
try
{
hasMutex = mutex.WaitOne(TimeSpan.FromSeconds(30));
if (!hasMutex)
{
throw new TimeoutException("Mutexの取得がタイムアウトしました");
}
try
{
// ファイルの読み書き
await File.WriteAllTextAsync("sample.txt", "データ" + DateTime.Now);
string content = await File.ReadAllTextAsync("sample.txt");
Console.WriteLine($"読み取り結果 : {content}");
}
catch (IOException ex)
{
Console.WriteLine($"ファイルアクセスエラー : {ex.Message}");
throw;
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine($"アクセス権限エラー : {ex.Message}");
throw;
}
}
catch (AbandonedMutexException)
{
// 別プロセスが終了してMutexが放棄された場合の処理
Console.WriteLine("Mutexが放棄されました");
throw;
}
finally
{
if (hasMutex)
{
try
{
mutex.ReleaseMutex();
}
catch (ApplicationException ex)
{
// ReleaseMutexに失敗した場合の処理
Console.WriteLine($"Mutexの解放に失敗 : {ex.Message}");
}
}
}
}
catch (UnauthorizedAccessException ex)
{
// Mutex作成時のアクセス権限エラー
Console.WriteLine($"Mutexの作成に失敗 : {ex.Message}");
throw;
}
finally
{
mutex?.Dispose();
}
}
// 使用例
try
{
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
tasks.Add(AccessFileAsync());
}
await Task.WhenAll(tasks);
tasks.Clear();
}
catch (Exception ex)
{
Console.WriteLine($"実行中にエラーが発生 : {ex.Message}");
}
FileShareモードを細かく制御可能である。
ファイルシステムレベルでの排他制御が可能なため、ファイルシステムレベルでの厳密な制御が必要な場合にする。
※注意
ただし、長時間ファイルをロックする可能性があるため注意が必要である。
// ファイルシステムレベルでの排他制御を行う
public static async Task AccessFileAsync()
{
try
{
// FileShareNoneで他のプロセスからのアクセスを完全にブロック
using (FileStream fs = new FileStream("sample.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
{
using (StreamWriter writer = new StreamWriter(fs))
using (StreamReader reader = new StreamReader(fs))
{
try
{
// ファイルの書き込み
await writer.WriteLineAsync("データ" + DateTime.Now);
await writer.FlushAsync();
// ストリームの位置を先頭に戻す
fs.Position = 0;
// ファイルの読み取り
string content = await reader.ReadToEndAsync();
Console.WriteLine($"読み取り結果 : {content}");
}
catch (IOException ex)
{
Console.WriteLine($"ストリーム操作エラー : {ex.Message}");
throw;
}
}
}
}
catch (IOException ex)
{ // ファイルが他のプロセスによってロックされている場合
Console.WriteLine($"ファイルアクセスエラー : {ex.Message}");
throw;
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine($"アクセス権限エラー : {ex.Message}");
throw;
}
}
// 使用例
try
{
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
tasks.Add(AccessFileAsync());
}
await Task.WhenAll(tasks);
tasks.Clear();
}
catch (Exception ex)
{
Console.WriteLine($"実行中にエラーが発生 : {ex.Message}");
}