bucket-sort logo bucket-sort

プログラミングとインフラエンジニアリングの覚え書き

  • Posts
  • About
  • Contact
  1. Home
  2. All Posts
  3. [C#] スレッド同期と ThreadPool を理解する

[C#] スレッド同期と ThreadPool を理解する

Jun 27, 2026 C# , .NET bucket-sort

前回は、Thread クラスを使ったマルチスレッド処理の基本を見ました。

今回は、複数のスレッドが同じデータを扱うときに必要になる 同期 と、.NET が管理する ThreadPool について整理します。

共有データと競合状態

複数スレッドから同じ変数を更新すると、期待通りの結果にならないことがあります。

using System;
using System.Threading;

int counter = 0;

void CountUp()
{
    for (int i = 0; i < 100_000; i++)
    {
        counter++;
    }
}

Thread t1 = new Thread(CountUp);
Thread t2 = new Thread(CountUp);

t1.Start();
t2.Start();

t1.Join();
t2.Join();

Console.WriteLine(counter);

counter++ は「値を読む」「1 を足す」「値を書き戻す」という複数の処理に分解できます。
その途中に別スレッドが割り込むと、更新が失われることがあります。

このような状態を 競合状態 と呼びます。

lock キーワードで同期する

もっともよく使う同期方法の 1 つが lock です。

using System;
using System.Threading;

object gate = new object();
int counter = 0;

void CountUp()
{
    for (int i = 0; i < 100_000; i++)
    {
        lock (gate)
        {
            counter++;
        }
    }
}

Thread t1 = new Thread(CountUp);
Thread t2 = new Thread(CountUp);

t1.Start();
t2.Start();

t1.Join();
t2.Join();

Console.WriteLine(counter);

lock ブロックには、同時に 1 つのスレッドだけが入れます。
これにより、counter++ の途中で別スレッドが割り込むことを防げます。

lock に使うオブジェクトは、通常 private readonly object として用意します。
文字列や this をロック対象にすると、外部コードと意図せず同じロックを共有する危険があるため避けます。

Monitor 型で同期する

lock 文は、内部的には Monitor.Enter と Monitor.Exit を使っています。
明示的に書くと、次のような形になります。

using System;
using System.Threading;

object gate = new object();
int counter = 0;

void CountUp()
{
    for (int i = 0; i < 100_000; i++)
    {
        bool lockTaken = false;

        try
        {
            Monitor.Enter(gate, ref lockTaken);
            counter++;
        }
        finally
        {
            if (lockTaken)
            {
                Monitor.Exit(gate);
            }
        }
    }
}

Thread t1 = new Thread(CountUp);
Thread t2 = new Thread(CountUp);

t1.Start();
t2.Start();

t1.Join();
t2.Join();

Console.WriteLine(counter);

通常は lock を使えば十分です。
Monitor を直接使うのは、タイムアウト付きでロックを試したい場合など、より細かい制御が必要なときです。

if (Monitor.TryEnter(gate, TimeSpan.FromSeconds(1)))
{
    try
    {
        Console.WriteLine("ロックを取得できました。");
    }
    finally
    {
        Monitor.Exit(gate);
    }
}
else
{
    Console.WriteLine("ロックを取得できませんでした。");
}

Interlocked 型でアトミック操作を行う

単純なカウントアップであれば、lock より Interlocked の方が簡潔です。

using System;
using System.Threading;

int counter = 0;

void CountUp()
{
    for (int i = 0; i < 100_000; i++)
    {
        Interlocked.Increment(ref counter);
    }
}

Thread t1 = new Thread(CountUp);
Thread t2 = new Thread(CountUp);

t1.Start();
t2.Start();

t1.Join();
t2.Join();

Console.WriteLine(counter);

Interlocked.Increment は、値の読み取り、加算、書き戻しを 1 つの不可分な操作として扱います。

代表的なメソッドは次の通りです。

Increment

整数値を 1 増やします。

Decrement

整数値を 1 減らします。

Add

指定した値を加算します。

Exchange

値を新しい値に置き換えます。

CompareExchange

現在値が期待値と一致した場合だけ、新しい値に置き換えます。

Timer コールバックを使う

System.Threading.Timer を使うと、一定時間ごとに処理を呼び出せます。

using System;
using System.Threading;

using Timer timer = new Timer(
    callback: _ => Console.WriteLine($"Tick: {DateTime.Now:HH:mm:ss}"),
    state: null,
    dueTime: TimeSpan.Zero,
    period: TimeSpan.FromSeconds(1));

Console.WriteLine("Enter キーで終了します。");
Console.ReadLine();

dueTime は最初に実行されるまでの待ち時間です。
period は 2 回目以降の実行間隔です。

Timer のコールバックは ThreadPool 上で実行されます。
そのため、コールバック内で共有データを扱う場合は、通常のマルチスレッド処理と同じように同期を考える必要があります。

スタンドアロン discard を使う

Timer などを「変数としては使わないが、破棄されないように保持したい」場合、discard の扱いに注意します。

using System;
using System.Threading;

Timer timer = new Timer(
    _ => Console.WriteLine("Tick"),
    null,
    TimeSpan.Zero,
    TimeSpan.FromSeconds(1));

Console.ReadLine();
timer.Dispose();

_ = new Timer(...) のように書くと、参照を保持していないため、意図しないタイミングで GC 対象になる可能性があります。
長く動かしたい Timer は、変数やフィールドに保持しておく方が安全です。

ThreadPool を理解する

ThreadPool は、.NET が管理する再利用可能なスレッドの集合です。

スレッドを毎回新規作成するとコストがかかります。
ThreadPool はあらかじめ用意したスレッドを使い回すことで、短い処理を効率よく実行します。

using System;
using System.Threading;

ThreadPool.QueueUserWorkItem(_ =>
{
    Console.WriteLine($"ThreadPool thread: {Thread.CurrentThread.ManagedThreadId}");
});

Console.WriteLine($"Main thread: {Thread.CurrentThread.ManagedThreadId}");
Console.ReadLine();

ThreadPool のスレッドは、基本的にバックグラウンドスレッドです。
また、処理がいつ実行されるかは ThreadPool のスケジューリングに任されます。

ThreadPool を直接使う場面

現在の C# では、ThreadPool を直接使うより Task.Run を使うことが多いです。

using System;
using System.Threading;
using System.Threading.Tasks;

await Task.Run(() =>
{
    Console.WriteLine($"Task thread: {Thread.CurrentThread.ManagedThreadId}");
});

Task.Run も、通常は ThreadPool を使って処理を実行します。
そのため、ThreadPool は Task や Parallel の土台として理解しておくとよいです。

まとめ

複数スレッドで共有データを扱うときは、同期が必要になります。

lock はもっとも基本的な同期手段です。
Monitor は lock の裏側にある仕組みで、タイムアウトなどの細かい制御に向いています。
Interlocked は、単純な数値更新を安全かつ軽量に行うために便利です。

また、ThreadPool は、現代の .NET の並列処理を支える重要な仕組みです。
次回は、この ThreadPool の上に構築される Task、Parallel、PLINQ を見ていきます。

C# .NET Lock Monitor Interlocked ThreadPool Timer System.Threading
← [C#] マルチスレッド処理の基礎を理解する [C#] Task Parallel Library と PLINQ を理解する →

Related Posts

  • [C#] マルチスレッド処理の基礎を理解する Jun 26, 2026
  • [C#] Interlockedの使い方 Mar 27, 2026
  • [C#] 非同期処理における競合対策 Mar 21, 2026
  • [C#] プラグインで拡張できるアプリケーションを設計する Jul 9, 2026

Table of Contents

  • 共有データと競合状態
  • lock キーワードで同期する
  • Monitor 型で同期する
  • Interlocked 型でアトミック操作を行う
    • Increment
    • Decrement
    • Add
    • Exchange
    • CompareExchange
  • Timer コールバックを使う
  • スタンドアロン discard を使う
  • ThreadPool を理解する
  • ThreadPool を直接使う場面
  • まとめ

Recent Posts

  • [C#] プラグインで拡張できるアプリケーションを設計する Jul 9, 2026
  • [C#] 属性でコードに意味を与える Jul 8, 2026
  • [C#] 外部アセンブリを読み込み実行時に型を呼び出す Jul 7, 2026
  • [C#] リフレクションで型の中身を調べる Jul 6, 2026
  • [C#] 型メタデータとリフレクションの基礎を理解する Jul 5, 2026

Categories

  • C#120
  • .NET119
  • AWS27
  • Laravel16
  • Linux15
  • MySQL9
  • Apache8
  • PHP8
  • DynamoDB6
  • セキュリティ6
  • Nginx5
  • WordPress4
  • インフラ4
  • Hugo3
  • .NET Framework1
  • Aurora1
  • Diagnostics1
  • Filament1
  • Git1
  • SQS1

Tags

  • C#
  • .NET
  • AWS
  • Laravel
  • コレクション
  • PHP
  • セキュリティ
  • MySQL
  • Linux
  • パフォーマンス
  • Apache
  • System.Collections.Generic
  • デリゲート
  • Code Snippet
  • DynamoDB
  • LINQ
  • NoSQL
  • PHP-FPM
  • RDS
  • System.Collections
  • Windows
  • メモリ管理
  • リフレクション
  • DoS
  • Nginx
  • WordPress
  • メタデータ
  • ラムダ式
  • 監視
  • 設計
  • Amazon Linux 2023
  • Delegate
  • Docker
  • IDisposable
  • Ipset
  • Iptables
  • LINQ to Objects
  • OPCache
  • Pointer
  • Reflection
  • System.Collections.Specialized
  • Unsafe
  • Webサーバー
  • インターフェース
  • オブジェクト指向
  • クラス設計
  • デザインパターン
  • パターンマッチング
  • ポインター
  • 継承
Powered by Hugo & Explore Theme.