2011年6月21日火曜日

Java のスレッドプール

More Effective C#』の item 11 に、"Use the Thread Pool Instead of Creating Threads"というのがある。new で スレッドを生成するのではなく、.Net で提供されるプールを使えと。

で、昨日『Effective Java (2nd edition)』を調べていると、item 68 で、ThreadPoolExecutor が紹介されていたので、ちょっと試してみた。

====

以下のような状況を想定する。

  • クライアントが Socket をつなぐと、サーバは ServerSocket で accept()して、1から10までの整数乱数を 1000個返す。
  • クライアントはその整数を一つずつ読み取り、幾ばくかの処理時間を要する何らかの処理を行う。ここでは、読み込んだ整数をミリ秒の時間間隔と捉えて、その分だけ sleep() させるようなコードを書く事にした。

クライアントは以下のようなコード。

public class Receiver {

   private static final short LISTEN_PORT = 3434;
   private static final int CORE_SIZE = 1;
   private static final int MAX_SIZE = 100;
   private static final int KEEP_ALIVE = 10;

   private static final ExecutorService pool = 
      new ThreadPoolExecutor(
         CORE_SIZE, MAX_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, 
         new LinkedBlockingQueue());

   public static void main(String[] args) throws IOException {
      Socket socket = new Socket("localhost", LISTEN_PORT);
      DataInputStream in = new DataInputStream(socket.getInputStream());

      long start = System.nanoTime();
      for (int i = 0; i < 1000; ++i) test(in.readInt());
      System.out.println((System.nanoTime() - start) / 1000000000.0);

      in.close();
      socket.close();
  
      pool.shutdown();
   }

   private static void executeTimeConsumingProcess(int interval) {
      try {
         TimeUnit.MILLISECONDS.sleep(interval);
      } catch (Exception e) {}
   }

   private static void test(int interval) throws IOException {
       //ここを書き換えて比較する
   }
}

で、この test() メソッドの中身を変えて、①逐次実行(並列なし)、②スレッドを生成する方式、③スレッドプールを使う方式で比較してみる。(サーバー側は、DataOutputStream で整数を返すようなコードになるが、ほぼ自明なので省略。)

① まず、並列処理をしない逐次実行のパターン

private static void test(int interval) throws IOException {
   executeTimeConsumingProcess(interval);
}
実行時間は、約 5.6秒と出た。平均 5ms の処理が1000回なので、だいたい想定どおりの結果。

② 次に、スレッドを生成するパターン。

private static void test(final int interval) throws IOException {
   new Thread(new Runnable() {
      @Override public void run() {
         executeTimeConsumingProcess(interval);  
      }
   }).start();
}
これは約 0.14秒と出た。当然ながら、逐次実行よりは大幅に速い。

③ 最後に、スレッドプールを使うパターン。

private static void test(final int interval) throws IOException {
   pool.execute(new Runnable() {
      @Override public void run() {
         executeTimeConsumingProcess(interval);
      }
   });
}

結果は約 0.028秒で、スレッド生成の5倍のスピードが得られた。やっぱり、それなりにパフォーマンスは良いらしい。

====

プールの初期サイズ、最大サイズなどを調整すると、若干値が変わってくる。実務で使うときは、実行環境に応じて調整できるような仕組みを作っておくと良いかもしれない。

ThreadPoolExecutor よりさらに手軽に使えるスレッドプールが、Executors の newCashedThreadPool() や newFixedThreadPool() などのメソッドで得られるが、一応試してみたところ、ThreadPoolExecutor を直に使うより、若干遅かった(倍くらいの所要時間)。もちろん、簡易な分、調整は利かない。

0 件のコメント:

コメントを投稿