C#でマルチスレッド処理や非同期処理を行う際、避けて通れないのが「排他制御(Exclusive Control)」です。
複数のスレッドが同時に同じデータやファイルにアクセスすると、データの不整合やファイルの破損、最悪の場合はアプリケーションのクラッシュを引き起こします。
「lockステートメントとMutexはどう違うの?」
「async/awaitの中でロックを使いたい」
こうした悩みを解決するために、この記事ではC#における排他制御の基本概念から、lock ステートメントを使った手軽な実装、プロセス間排他を実現する Mutex、そして非同期メソッドでも使える SemaphoreSlim まで、現在の開発現場で役立つ知識を徹底解説します。
![]() 執筆者:マヒロ |
|
排他制御とは?なぜ必要なのか
排他制御とは、ある処理を行っている間、他の処理が割り込まないように「ロック(鍵)」をかける仕組みのことです。
例えば、銀行口座の残高を更新する処理を考えてみましょう。
「残高を読み取る」→「金額を足す」→「残高を書き込む」という一連の流れの途中で、別のスレッドが割り込んで残高を読み取ってしまうと、計算結果がおかしくなってしまいます(競合状態)。
これを防ぐために、「私が使い終わるまで、誰も手出ししないでね」と宣言するのが排他制御です。
最も基本的な排他制御「lock」ステートメント
C#でスレッド間の排他制御を行う最も一般的で簡単な方法は、lock ステートメントを使用することです。
lock ブロックで囲まれた範囲は、一度に一つのスレッドしか実行できません。
lockの基本的な書き方
using System;
using System.Threading;
using System.Threading.Tasks;
class BankAccount
{
private int balance = 1000;
// ロック専用のオブジェクトを用意する
private readonly object lockObj = new object();
public void Deposit(int amount)
{
// ここから排他制御を開始
lock (lockObj)
{
Console.WriteLine($"現在の残高: {balance}, 預入額: {amount}");
// 処理中に他のスレッドが来ても、ここで待たされる
balance += amount;
Console.WriteLine($"更新後の残高: {balance}");
}
// ここでロック解除
}
}
class Program
{
static void Main()
{
BankAccount account = new BankAccount();
// 複数のスレッドから同時にアクセスさせる
Parallel.Invoke(
() => account.Deposit(100),
() => account.Deposit(200),
() => account.Deposit(300)
);
}
}
ソースコードの解説
- ロックオブジェクト:
private readonly object lockObj = new object();で、鍵となるオブジェクトを作成します。thisやtypeof(MyClass)をロックに使うのは、外部からのロックと干渉するリスクがあるため推奨されません。 - lock (lockObj): このブロックに入ろうとしたスレッドは、
lockObjが誰にも使われていなければブロックに入り、ロックをかけます。もし他のスレッドが使用中であれば、ロックが解除されるまで待機します。
この仕組みにより、データの整合性が保たれます。
プロセス間での排他制御には「Mutex」
lock ステートメントは、あくまで「同じアプリケーション(プロセス)内のスレッド間」での排他制御にしか使えません。
「アプリを二重起動させたくない(多重起動防止)」といった、異なるプロセス間での排他制御には Mutex(ミューテックス)クラスを使用します。
アプリの多重起動を防止するサンプル
using System;
using System.Threading;
class Program
{
static void Main()
{
// Mutex名を指定して作成(Global\を付けると全ユーザーで共有)
string mutexName = "Global\\MyUniqueAppMutex";
bool createdNew;
// Mutexの取得を試みる
// createdNewがtrueなら、新しく作成された(=まだ起動していない)
using (var mutex = new Mutex(false, mutexName, out createdNew))
{
if (!createdNew)
{
// すでに起動している場合
Console.WriteLine("すでにアプリは起動しています。終了します。");
return; // アプリ終了
}
Console.WriteLine("アプリを起動しました。Enterキーで終了...");
Console.ReadLine();
}
}
}
new Mutex(..., "名前", out createdNew) で、システム全体で共有される名前付きのMutexを作成しようとします。
もし同じ名前のMutexが既に存在していれば、createdNew は false になります。
これを利用して、2つ目以降の起動をブロックすることができます。
非同期処理(async/await)での排他制御「SemaphoreSlim」
現代のC#開発では async/await を多用しますが、実は lock ステートメントの中では await を使うことができません(コンパイルエラーになります)。
非同期メソッド内で排他制御を行いたい場合は、SemaphoreSlim(セマフォスリム)クラスを使用します。
asyncメソッド内でのロックの実装
using System;
using System.Threading;
using System.Threading.Tasks;
class AsyncResource
{
// 初期値1、最大値1のセマフォ(=ロックと同じ動作)
private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
public static async Task AccessAsync(string name)
{
Console.WriteLine($"{name} がロック待機中...");
// ロックを取得(非同期に待機)
await semaphore.WaitAsync();
try
{
Console.WriteLine($"{name} がロックを取得!処理開始");
await Task.Delay(1000); // 重い処理のシミュレーション
Console.WriteLine($"{name} が処理終了");
}
finally
{
// 必ずfinallyで解放する
semaphore.Release();
Console.WriteLine($"{name} がロックを解放");
}
}
}
class Program
{
static async Task Main()
{
// 3つの処理を同時に開始
var t1 = AsyncResource.AccessAsync("Task A");
var t2 = AsyncResource.AccessAsync("Task B");
var t3 = AsyncResource.AccessAsync("Task C");
await Task.WhenAll(t1, t2, t3);
}
}
ソースコードの解説
SemaphoreSlim(1, 1): 同時にアクセスできる数を「1」に制限することで、排他制御として機能させます。await semaphore.WaitAsync(): ロックが空くのを非同期に待ちます。スレッドをブロックしないため、UIスレッドなどをフリーズさせずに待機できます。try-finally:lock構文と違って自動で解放されないため、例外が発生しても確実にRelease()が呼ばれるようにtry-finallyブロックで囲むのが鉄則です。
読み書きロック「ReaderWriterLockSlim」
「読み取りは同時に何人でもOKだけど、書き込み中だけは誰もアクセスさせたくない」というケースでは、ReaderWriterLockSlim を使うとパフォーマンスが向上します。
頻繁にデータを参照するキャッシュシステムなどで有効です。
using System;
using System.Threading;
class CacheData
{
private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
private int data = 0;
// 読み取り(複数スレッド同時実行OK)
public int Read()
{
rwLock.EnterReadLock();
try
{
return data;
}
finally
{
rwLock.ExitReadLock();
}
}
// 書き込み(他をブロックする)
public void Write(int value)
{
rwLock.EnterWriteLock();
try
{
data = value;
}
finally
{
rwLock.ExitWriteLock();
}
}
}
C#のスキルを活かして年収を上げる方法
以上、C#での排他制御の基本と実装パターンについて解説してきました。
なお、C#のスキルがある場合には、「転職して年収をアップさせる」「副業で稼ぐ」といった方法を検討するのがおすすめです。
業務システム開発やアプリ開発、ゲーム開発において需要の高いC#を扱えるエンジニアは、転職によって数十万円の年収アップはザラで、100万円以上年収が上がることも珍しくありません。
なお、転職によって年収を上げたい場合は、エンジニア専門の転職エージェントサービスを利用するのが最適です。
併せて、副業案件を獲得できるエージェントにも登録しておくと、空いている時間を活かして稼げるようなC#の案件を探しやすくなります。
転職エージェントも副業エージェントも、登録・利用は完全無料なので、どんな求人や副業案件があるのか気になる方は、気軽に利用してみるとよいでしょう。



