C# によるマルチスレッドアプリケーションと lock 文による排他制御

ここでは、C# でのマルチスレッドプログラミングの最初の話題として、 マルチスレッドプログラムで排他制御しないと、簡単にデータが壊れちゃいますよ、ということを実験しながら確認して、 それに対する対策を簡単に紹介します。

マルチスレッドプログラムでデータが壊れる例

まずは、次のコードを実行してみましょう。

スレッドを 15個作り、それぞれのスレッドから、同じ static で定義した val という int 型の変数を +1 (インクリメント) します。

using System;
using System.Threading;

class Program {

    const int THREAD_COUNT = 15;
    static int val = 0;

    static void Main(string[] args) {

        var startTick = Environment.TickCount;
        var threads = new Thread[THREAD_COUNT];

        for (var i = 0; i < threads.Length; i++) {
            var threadStart = new ThreadStart(threadEntry);
            var thread = new Thread(threadStart);
            threads[i] = thread;
        }

        for (var i = 0; i < THREAD_COUNT; i++) {
            threads[i].Start();
        }

        for (var i = 0; i < THREAD_COUNT; i++) {
            threads[i].Join();
        }

        var endTick = Environment.TickCount;
        Console.WriteLine("Result val={0} elapsed time={1}[ms]",
            val, endTick - startTick);

    }

    static void threadEntry() {
        for (var m = 0; m < 100000; m++) {
            val++;
        }
    }

}

スレッド 15 個で、それぞれのスレッドが 10万回ずつ、val をプラス 1 しようと試みるので、最終的な値としては 1500000 になるはずですが、 実際は次のようにわけのわからない数字が結果となって出てきます。

複数のスレッドから同じ変数にアクセスしたので、データが壊れたのですね。

そこで、マルチスレッドの時は、同じ変数にアクセスするのは原則的にひとつのスレッドだけに限定しないといけないのです。(もちろん、その変数に何か工夫がしてあって、 同時アクセスを許可しているとか、そういう特別な場合は除きます。)

こうした制御を一般的に「排他制御」といいます。

lock 文を利用した排他制御

そこで、C# では手っ取り早く排他制御する方法としては、lock文が使えます。

lock とコードブロックを組み合わせておき、lock にオブジェクトを渡すと、そのオブジェクトを取得したスレッドだけが、そのコードブロックに入れるようになります。そして、コードブロックから出るときに、そのオブジェクトを解放します。

例えて言えば、トイレのカギみたいなものです。カギが一つだけあって、そのカギを持った人だけがトイレ(コードブロック)に入ることができ、出てきたら、他の人にカギを渡せば他の人がトイレに入れる・・・なんとなく、いいたいことは伝わったでしょうか。

さて、lock の具体的な使用方法ですが、とても簡単です。渡すオブジェクトはなんでもいいので、次のように何もない空の Object を作り、lock に渡します。これだけで、そのコードブロックにアクセスできるのが一つのスレッドに限定できます。簡単でいいですね。

using System;
using System.Threading;

class Program {

    const int THREAD_COUNT = 15;
    static int val = 0;
    static Object lockObj = new Object();

    static void Main(string[] args) {

        var startTick = Environment.TickCount;
        var threads = new Thread[THREAD_COUNT];

        for (var i = 0; i < threads.Length; i++) {
            var threadStart = new ThreadStart(threadEntry);
            var thread = new Thread(threadStart);
            threads[i] = thread;
        }

        for (var i = 0; i < THREAD_COUNT; i++) {
            threads[i].Start();
        }

        for (var i = 0; i < THREAD_COUNT; i++) {
            threads[i].Join();
        }

        var endTick = Environment.TickCount;
        Console.WriteLine("Result val={0} elapsed time={1}[ms]",
            val, endTick - startTick);

    }

    static void threadEntry() {
        for (var m = 0; m < 100000; m++) {
            lock (lockObj) {
                val++;
            }
        }
    }

}

実際に実行すると、確かに 1500000 になりました。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 C# 入門