2015年10月19日月曜日

マルチスレッドプログラミング(詳細版)

スレッドとはなにか


スレッドとは一度に複数の処理を行いたいときに使うものです。皆さんご存知のとおり、プログラムはコードの上から下に順番にコンピューターに処理されます。これは、あなたの書いた命令(コード)をロボット(コンピューター)がひとつずつ処理していくのだというふうにも考えられます。


でも、一人のロボットだけだとどうしても仕事が多すぎて処理しきれないことがあります。たとえばポートを検索してサーバーのあるポートを表示するプログラムを考えてみましょう。このとき、プログラムの処理をするのは1人のロボットですから、この1人のロボットが検索を実行している間、このプログラムはユーザーの操作を受け付けるロボットがいなくなってしまいます。この結果、このプログラムは検索が終了するまで、ユーザーの操作を受け付けなくなります。

100とか200のポートを検索すると長い時間がかかりますから、それまでずっとじっと待ってるのは現実的ではありません。すごく不便ですね。ユーザーの気が変わって検索を中止したり、途中結果をコピペしたりしたいときもあるでしょう。

こんなときはどうすればいいのでしょうか。こんなときに役立つのがマルチスレッドプログラミングです。ユーザーからの操作を受け付けるロボットはそのまま仕事をさせておいて、検索を行うための2人めのロボットを追加したらどうでしょうか。ロボットが2人いれば、ユーザーから受け付けた操作を処理すると同時に検索もすることができますね。これがマルチスレッドプログラミング(multi threading)です。マルチスレッドプログラミングにより処理をするロボットは何人でも増やせますし、いくつもの処理を同時に扱うことができます。


マルチスレッドプログラミングの方法


マルチスレッドプログラミングには2つの方法があります。ひとつはThreadクラスを継承する方法です。

class ThreadTest {
    public static void main(String[] args) {
        ThreadTestThread tt = new ThreadTestThread();
        tt.start();
    }
}

class ThreadTestThread extends Thread {
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println( i + " Hello World");
        }
    }
}

javaにあらかじめ用意されているThread クラスを継承するThreadTestThread クラスを定義し、そのオブジェクトを作成、そのあとにstart() メソッドを呼び出すことで、ThreadTestThread クラスの run() メソッドが実行されます。

2つ目はRunnable インタフェースを実装する方法です。javaではクラスは多重継承できないので、すでにひとつのクラスを継承していてなおかつマルチスレッドプログラミングを使いたい場合はこちらの方法を使います。

class ThreadTest {
    public static void main(String[] args) {
        RunnableTestThread tt = new RunnableTestThread();
        Thread t = new Thread(tt);
        t.start();
    }
}

class RunnableTestThread implements Runnable {
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(i + " Hello World");
        }
    }
}

RunnableTestThreadクラスがRunnableインターフェイスを実装しています。そしてこのRunnableTestThreadクラスのオブジェクトを作成し、作成したオブジェクトを引数にしてThreadクラスのオブジェクトを作成します。そのオブジェクトのstartメソッドを使うことでRunnableTestThreadクラスのrun(){}の内容が実行されます。

排他制御(synchronized)

しかし、スレッドをたくさん作って実行すると、割り込みが発生することがあります。(以下はとほほのJava入門様 url: http://www.tohoho-web.com/java/thread.htm を参考に書いたコードです)

class SyncTest {
    static Counter counter = new Counter();

    public static void main(String[] args) {

        // スレッドを1000個作成する
        MyThread[] threads = new MyThread[1000];
        for (int i = 0; i < 1000; i++) {
            threads[i] = new MyThread();
            threads[i].start();
        }

        // スレッドがすべて終了するのを待つ
        for (int i = 0; i < 1000; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }

        // カウンターを表示する
        System.out.println(SyncTest.counter.count);
    }
}

// スレッド
class MyThread extends Thread {
    public void run() {
        SyncTest.counter.countUp();
    }
}

// カウンター
class Counter {
    int count;
    void countUp() {
        System.out.print("Hello, ");
        int n = count;            // カウンターを読み出して
        System.out.print("World");
        count = n + 1;            // 加算して書き戻す
        System.out.println("!!");
    }
}

実行結果。1000になるはずが1000にならない。


結果をよく見てみると混乱している箇所がある

結果は1000になるはずですが、実行しても1000になりません。これは複数の処理を同時に実行して混乱が発生しているためです。コードの赤い部分、Counterクラスではカウンターを読み出して加算して書き戻していますが、ここを複数のスレッドが同時に処理すると、混乱(割り込み)が発生します。

複数のスレッドが同時にこのCounterクラスを実行した結果、カウンター値を読み出して設定するまでの間に他のスレッドが割り込んでしまい、同じ値を読み出して同じ値を設定してしまうのです。

このような割り込みを防ぐのが排他制御(synchronized)です。以下のようにコードを書き換えると以下のコードはいちどにひとつのスレッドしか実行できなくなり、上記のような混乱がなくなります。

void countUp() {
        synchronized (this) {
            System.out.print("Hello, ");
            int n = count;            // カウンターを読み出して
            System.out.print("World");
            count = n + 1;            // 加算して書き戻す
            System.out.println("!!");
        }
    }

これが排他統御の一例ですが、ほかにもこんな書き方があるようです。

class Global {
    static Object lock = new Object();
}

class Counter {
    void countUp() {
        synchronized (Global.lock) {
            // 排他制御を行いたい箇所
        }
    }
}



参考文献
とほほのJava入門 url: http://www.tohoho-web.com/java/thread.htm 2015年10月19日閲覧。