SwingWorker とスレッド数の上限

JDK 6 で追加された SwingWorker は、バックグラウンドスレッドと EDT との通信が必要な場合に非常に有用なクラスで、Swing プログラムを書くときにはよく使う。SwingWorker の詳しい紹介は他の web リソースに任せるとして、今回は SwingWorker を使う上でハマったこと - タイトルにあるようなスレッド数の上限にまつわることを紹介してみる。

SwingWorker は、大体以下のような使いかたをする。

new SwingWorker<Void, Void>() {
  @Override
  protected Void doInBackground() throws Exception {
    heavyTask();
    return null;
  }
}.execute();

このような使いかたであれば、基本的に注意するべき点は (あんまり) ない。問題は以下のような使いかた。

new SwingWorker<Void, Void>() {
  @Override
  protected Void doInBackground() throws Exception {
    try {
      while (true) {
        Thread.sleep(1000);
        if (isCancelled()) {
          break;
        }
        heavyTask();
        publish();
      }
    } catch (InterruptedException exit) {
    }
    return null;
  }
  @Override
  protected void process(List<Void> chunks) {
    updateView();
  }
}.execute();

要約すると、「定期的に起き上がって処理を実行し、ビューを更新する無限ループを実行する SwingWorker」みたいな感じ。ちなみにこんな感じの SwingWorker であっても「普通に」使うぶんには特に問題ない。

じゃあどういうときに問題なのか。それは 2 つ目のような SwingWorker を 10 個以上同時実行したとき。これをやると、実行中の SwingWorker のいずれかを殺すまで、次の SwingWorker の doInBackground が始まらないという問題が起こる。つまりこんな感じ↓

for (int i = 0; i < 10; ++i) {
  new SwingWorker<Void, Void>() {
    @Override
    protected Void doInBackground() throws Exception {
      try {
        while (true) {
          Thread.sleep(1000);
          if (isCancelled()) {
            break;
          }
          heavyTask();
          publish();
        }
      } catch (InterruptedException exit) {
      }
      return null;
    }
    @Override
    protected void process(List<Void> chunks) {
      updateView();
    }
  }.execute();
}
new SwingWorker<Void, Void>() {
  @Override
  protected Void doInBackground() throws Exception {
    heavyTask(); // ここは実行されない
  }
}.execute();

なんでこういう問題が起こるかというと、SwingWorker の execute メソッドの実装が関わってくる。execute メソッドで何をやっているかというと、おおざっぱに言えば「スレッド数の上限が 10 の static な ThreadPoolExecutor に自身を投げる」ということをやっている。つまり SwingWorker の execute メソッドを使うと、同時実行されるタスクが 10 個までという制限をかけられることになる。

ではどうしたらいいのか。要は execute メソッド内で使われている「スレッド数の上限が 10 の static な ThreadPoolExecutor」を使わなければいいだけ。つまり SwingWorker の execute メソッドを使うのではなく、独自に用意した Executor の execute メソッドに SwingWorker を渡せばいいってこと。

Executor exec = Executors.newCachedThreadPool();
for (int i = 0; i < 10; ++i) {
  exec.execute(new SwingWorker<Void, Void>() {
    @Override
    protected Void doInBackground() throws Exception {
      try {
        while (true) {
          Thread.sleep(1000);
          if (isCancelled()) {
            break;
          }
          heavyTask();
          publish();
        }
      } catch (InterruptedException exit) {
      }
      return null;
    }
    @Override
    protected void process(List<Void> chunks) {
      updateView();
    }
  });
}
exec.execute(new SwingWorker<Void, Void>() {
  @Override
  protected Void doInBackground() throws Exception {
    heavyTask(); // ここも実行される
  }
});

これで 10 個以上のタスクを同時にバックグラウンドスレッド上で実行できるようになった。めでたしめでたし。

でも、この方法を使うのは本当に必要な場合だけに絞ったほうがいいだろうとも思う。特に一番最初に示した SwingWorker の使いかただけなのであれば、今回紹介したテクニックは無用ではないかなと。10 個以上のタスクをバックグラウンドで同時実行することなんて、よっぽどだと思うし…。もし仮にそういう状況に (一時的に) 陥ったとしても、それは 10 個のスレッドでがんばって処理してもらえば十分なんでねーかなと。