マルチスレッド処理の基礎(5) - フィールドの取り扱い Part 1

これから数回に分けて、フィールドを更新・参照する処理をスレッドセーフにする方法を説明します。

単一のフィールドを固定値で更新する場合はvolatile修飾子をつける

処理終了要求フラグのように、単一のフィールドをあるスレッドが固定値で更新、他のスレッドがそのフィールドを参照する場合には、フィールドにvolatile修飾子をつけます。volatile修飾子をつけない場合、コンパイル時や実行時の最適化により、プログラムが開発者の意図通り動作しない可能性があります。
なお、volatile修飾子をつけるフィールドの型は、基本的には組み込みデータ型や不変クラスなどのスレッドセーフな型のみを用いるようにします。

public class LoopAction
{
    private readonly Action action;
    private volatile bool shutdownRequested;

    public LoopAction(Action action)
    {
        this.action = action;
        this.shutdownRequested = false;
    }

    public void Run()
    {
        while (!shutdownRequested)
        {
            action();
        }
    }

    public void ShutdownRequest()
    {
        shutdownRequested = true;
    }
}

internal class Program
{
    internal static void Main(string[] args)
    {
        Thread.CurrentThread.Name = "Main";

        var loop = new LoopAction(() =>
        {
            Thread.Sleep(3000);
            Console.WriteLine("{0}: Running...", Thread.CurrentThread.Name);
        });

        var t = new Thread(loop.Run);
        t.Name = "Loop";
        t.Start();

        Thread.Sleep(9500);

        loop.ShutdownRequest();
        Console.WriteLine("{0}: Shutdown request.", Thread.CurrentThread.Name);

        t.Join();
        Console.WriteLine("{0}: Shutdown completed.", Thread.CurrentThread.Name);
    }
}

単一のフィールドを参照、計算後に更新する場合はInterlockedクラスを使用する

数値のカウントや合計のように、単一のフィールドを複数のスレッドで参照、計算、更新する場合には、Interlockedクラスのメソッド経由でフィールドにアクセスします。Interlockedクラスのメソッドを使用しない場合、競合によりフィールドが予期しない値になる可能性があります。
なお、Interlockedクラスのメソッドを使用する場合には、フィールドへのvolatile修飾子は不要です。

public class SomeTask
{
    private static long totalTicks = 0;

    public static TimeSpan TotalTime
    {
        get
        {
            var ticks = Interlocked.Read(ref totalTicks);
            return new TimeSpan(ticks);
        }
    }

    private readonly int id;

    public SomeTask(int id)
    {
        this.id = id;
    }

    public void Run()
    {
        int time = new Random(id).Next(300);

        var sw = new Stopwatch();
        sw.Start();
        Thread.Sleep(time);
        sw.Stop();

        var elapsed = sw.Elapsed;
        Interlocked.Add(ref totalTicks, elapsed.Ticks);

        Console.WriteLine("Task[{0:00}]: {1}", id, elapsed);
    }
}
internal class Program
{
    internal static void Main(string[] args)
    {
        var count = 10;

        var threads = Enumerable.Range(0, count).Select(i =>
        {
            var task = new SomeTask(i);
            var t = new Thread(task.Run);
            return t;
        }).ToArray();

        foreach (var t in threads)
        {
            t.Start();
        };

        foreach (var t in threads)
        {
            t.Join();
        };

        Console.WriteLine("Total:   {0}", SomeTask.TotalTime);
    }
}

複数のフィールドを更新・参照するする可能性がある処理はlockステートメントを使用する

複数フィールドの連続参照・更新処理や、スレッドセーフでないオブジェクトへのプロパティアクセス・メソッド呼び出し等の複雑な処理を行う場合には、lockステートメントを使用します。lockステートメントを使用しない場合、競合によりプログラムが開発者の意図通り動作しない可能性があります。
また、lockステートメントを使用する際には、以下の項目を守るようにします。その他にもいくつか注意すべき点はありますが、それについては別途解説を行う予定です。

  1. lockで指定するインスタンスは、自クラス内の専用のreadonly objectフィールドに保持し、アクセス修飾子はprivateとする(派生クラスとの共有が必要な場合はprotectedでもよいが、基本的には避ける)
  2. 静的フィールドを保護する場合は静的フィールド、インスタンスフィールドを保護する場合はインスタンスフィールドを使用する
  3. 同一のフィールドにアクセスするアプリケーション内のすべての処理を、同じインスタンスを指定したlockステートメントの中で行うようにする
public class SomeTaskStatistics
{
    private readonly int count;
    private readonly TimeSpan totalTime;

    public SomeTaskStatistics(int count, TimeSpan totalTime)
    {
        this.count = count;
        this.totalTime = totalTime;
    }

    public int Count { get { return count; } }
    public TimeSpan TotalTime { get { return totalTime; } }
    public TimeSpan AverageTime { get { return new TimeSpan(TotalTime.Ticks / Count); } }
}

public class SomeTask
{
    private static readonly object StatisticsLock = new object();
    private static int count;
    private static TimeSpan totalTime;

    public static SomeTaskStatistics Statistics
    {
        get
        {
            lock (StatisticsLock)
            {
                return new SomeTaskStatistics(count, totalTime);
            }
        }
    }

    private static void Done(TimeSpan time)
    {
        lock (StatisticsLock)
        {
            count++;
            totalTime += time;
        }
    }

    private readonly int id;

    public SomeTask(int id)
    {
        this.id = id;
    }

    public void Run()
    {
        int time = new Random(id).Next(300);

        var sw = new Stopwatch();
        sw.Start();
        Thread.Sleep(time);
        sw.Stop();

        var elapsed = sw.Elapsed;
        Done(elapsed);

        Console.WriteLine("Task[{0:00}]: {1}", id, elapsed);
    }
}

internal class Program
{
    internal static void Main(string[] args)
    {
        var count = 10;

        var threads = Enumerable.Range(0, count).Select(i =>
        {
            var task = new SomeTask(i);
            var t = new Thread(task.Run);
            return t;
        }).ToArray();

        foreach (var t in threads)
        {
            t.Start();
        };

        foreach (var t in threads)
        {
            t.Join();
        };

        var info = SomeTask.Statistics;

        Console.WriteLine("Count:{0}, Total: {1}, Average:{2}", info.Count, info.TotalTime, info.AverageTime);
    }
}

※上記程度の処理であれば、lockではなくInterlocked.CompareExchange()を用いる方法でも実現は可能です。