JavaScript でマルチスレッド・デザインパターン

JavaScriptデザインパターンを書いてみる。とはいえ、いまさらFactoryパターンなど書いても面白くないので、マルチスレッド・デザインパターンにする。

もちろん、JavaScriptの言語仕様にスレッドなどない。しかし、ブラウザ環境では実質的にスレッドが存在する。スレッドが発生する場面には2種類ある。1つは、イベントである。例えば、ユーザのクリック動作によってonclickイベントが発生した時、onclickイベントハンドラだけを処理するスレッドが発生するとみなせる。もう1つは、setTimeoutとsetIntervalメソッドである。これらのメソッドでは、一定時間後に、別の処理を実行させるが、この処理は現在の処理と並行的に実行される。つまり、スレッドとみなせる。個人的には、setTimeoutをアニメーションなどの小細工だけに使うのはもったいないと思う。setTimeoutの本領は、並行処理の利用によるレスポンス向上にある。JavaScriptでリッチ・クライアントを実現するには必要不可欠である。

今回は、Worker Threadパターンを書いてみた。とはいえ、ブラウザ環境のスレッドモデルはかなり貧弱である。当然、waitやnotifyもないし、そもそもsleepすらない。従って、無理もかなりあるが、容赦されたい。

var Thread = function(){};
Thread.prototype = {
  start: function(){
      setTimeout(this.run.bind(this), 10);
  }  
}
var Client = Class.create();
Client.prototype = (new Thread).extend({
    initialize: function(name, channel){
	this.channel = channel;
	this.name = name;
	this.count = 0;
	this.maxCount = 5;
    },
    run: function(){
	var request = new Request(this.name, this.count);
	if(this.channel.putRequest(this, request)){
	    this.count += 1;
	    if(this.count != this.maxCount){
		this.start();
	    }
	}
    }
})
var Worker = Class.create();
Worker.prototype = (new Thread).extend({
    initialize: function(name, channel){
	this.name = name;
	this.channel = channel;
    },
    run: function(){
	var request = this.channel.takeRequest(this);
	if(request){
	    request.execute(this);
	    this.start();
	}
    }
})
var Channel = Class.create();
Channel.prototype = {
    initialize: function(maxRequest, threads){
	this.requestQueue = new Array(maxRequest);
	this.head = this.tail = this.count = 0;
	this.waitSet = []
	this.threadPool = new Array(threads);
	for(var i = 0; i < threads; i++){
	    this.threadPool[i] = new Worker("Worker-" + i, this);
	}
    },
    startWorkers: function(){
	for(var i = 0; i < this.threadPool.length; i++){
	    this.threadPool[i].start();
	}
    },
    wait: function(thread){
	this.waitSet.push(thread);
    },
    notifyAll: function(){
	var set = this.waitSet;
	this.waitSet = [];
	for(var i = 0; i < set.length; i++){
	    set[i].start();
	}
    },
    putRequest: function(thread, request){
	if(this.count >= this.requestQueue.length){
	    this.wait(thread);
	    return null;
	}
	this.requestQueue[this.tail] = request;
	this.tail = (this.tail + 1) % this.requestQueue.length;
	this.count++;
	this.notifyAll();
	return true;
    },
    takeRequest: function(thread){
	if(this.count <= 0){
	    this.wait(thread);
	    return null;
	}
	var request = this.requestQueue[this.head];
	this.head = (this.head + 1) % this.requestQueue.length;
	this.count--;
	this.notifyAll();
	return request;
    }
}

例によって、prototype,jsを使っている。
ClientとWorkerはThreadのサブクラスである。Threadクラスでは、startメソッドによって、runメソッドを実行するスレッドを発生させる。

そして、重い処理を実行するRequestクラスと、Mainクラス、それにログを記録するLoggerクラスを追加する。

var Request = Class.create();
Request.prototype = {
    initialize: function(name, number){
	this.name = name;
	this.number = number;
    },
    execute: function(thread){
	for(var i = 0; i< 100000; i++){
	    Math.log(i);
	}
	logger.puts(thread.name + " execute " + this.name + " " + this.number);
    }
}
var Main = Class.create();
Main.prototype = {
    initialize: function(){
	var channel = new Channel(5, 2);
	channel.startWorkers();
	( new Client("Alice", channel) ).start();
	( new Client("Bobby", channel) ).start();
	( new Client("Chris", channel) ).start();
    }
}
var Logger = Class.create();
Logger.prototype = {
    initialize: function(element){
	this.log = "";
	this.element = $(element);
    },
    puts: function(str){
	this.log += str + "<br />";
	this.element.innerHTML = this.log;
    }
}
var logger = new Logger("log");
var main = new Main;

これらのコードをworker.jsに記述して、

<html>
<body>
<div id = "log">
</div>
</body>
<script language="JavaScript" src="js/prototype.js"></script>
<script language="JavaScript" src="js/worker.js"></script>
</html>

というhtmlファイルを開くと、

Worker-0 execute Alice 0
Worker-1 execute Bobby 0
Worker-0 execute Chris 0
Worker-1 execute Alice 1
Worker-0 execute Bobby 1
Worker-1 execute Chris 1
Worker-0 execute Alice 2
Worker-1 execute Bobby 2
Worker-0 execute Chris 2
Worker-1 execute Alice 3
Worker-0 execute Bobby 3
Worker-1 execute Alice 4
Worker-0 execute Bobby 4
Worker-1 execute Chris 3
Worker-0 execute Chris 4

という実行結果が得られる。このとき、1行ずづ処理結果が表示される。これは、Workerで、1つのRequestの処理が終了したならば、次のRequestを発生されるスレッドを発生させている為である。こうしないと、一度に複数の処理結果が表示され、応答速度が低下してしまう。

Worker Threadパターンを用いる主な利点は次の3点である。

  1. 処理の要求者(Client)・内容(Request)・実行者(Worker)を分離できる。
  2. 同時に実行する処理数の上限を制限できる。
  3. 1つのRequestが終了しだい、結果をすぐに反映できる。

参考書籍