[JavaScript] JavaScriptはシングルスレッド:非同期処理の仕組み

2012 年 4 月 18 日

【2013.03.13 修正】setTimeoutの引数の順序が間違っていた問題を修正

【シングルスレッドとマルチスレッド】

シングルスレッドは、処理の流れがひとつのみ。

マルチスレッドは、二つ以上の処理が同時並行に行なわれる。

たとえば、以下のdoThredA()、doThreadB()がそれぞれ別スレッドで実行された場合、numの値がどのタイミングで何になっているかはわからない。

var num = 0;

function doThredA() {
	for (var i = 0; i < 1000; ++i) {
		++num;
	}
}

function doThredB() {
	for (var i = 0; i < 1000; ++i) {
		++num;
	}
}

【JavaScriptはシングルスレッド+非同期処理】

非同期処理がからむとやや複雑になってくるが、あくまでシングルスレッドだということを踏まえていれば大丈夫。

function doSample() {
	setTimeout(onTimeout, 1000);
	
	function onTimeout() {
		alert("fin");
	}
}

doSample();

この例の場合、onTimeout()はdoSample()が終了したあとで実行される。仮にsetTimeout(onTimeout, 0)としても同様。

var msg = "";

function doSample() {
	setTimeout(onFirst, 1000);
	setTimeout(onSecond, 50);
	
	function onFirst() {
		msg += "onFirst\n";
	}
	
	function onSecond() {
		msg += "onSecond\n";
	}
}

doSample();
alert(msg);

こちらは、setTimeout(onFirst, 1000)のほうが先に実行されているが、onSecond()がdoSample()の次に実行され、onFirst()はさらにそのあとになる。

doSample() – onSecond() – onFirst()

このように、JavaScriptはシングルスレッドという前提を踏まえれば、関数単位で順番に実行されていくことがわかる。

基本的にスレッドやコルーチンの仕組みはないので、関数内の処理の途中で別の非同期処理が勝手に入ることはない。

非同期処理では、次に実行する関数の待ち行列(キュー)のようなものができており、非同期処理が実行されたタイミングに関係なく、準備ができた関数からその待ち行列に並んでいく。

上の例でいえば、setTimeout()が実行されたタイミングは関係ないということ。onloadやXmlHttpRequestなどのイベント処理でもまったく同じ。

難しいようにも思えるが、以下の二点を踏まえていけば問題ない。

  • 関数単位で順番に実行されていく
  • 非同期処理は、実行の準備ができたコールバック関数が待ち行列をつくり、現在実行中の関数の処理が終了してから、順に関数が実行されていく

注意が必要なのは、C++的に自前で単純なループを回して、フレーム処理などをやろうとした場合。

function doMainLoop() {
	setTimeout(onTimeout, 100);
	
	var preTime = 0
	
	while (true) {
		var curTime = new Date().getTime();
		
		if (curTime - preTime > 16) {
			update();
			draw();
			
			preTime = curTime;
		}
	}
	
	function onTimeout() {
		alert("fin");
	}
}

while (true)のループが回っているかぎりdoMainLoop()が終了しないので、onTimeout()は指定時間後もずっと呼ばれないまま。

よって、JavaScriptのようなシングルスレッド+非同期処理の仕組みでは、こういったコーディングの仕方をしてはいけない。

タイマーの注意点

setInterval()もsetTimeout()も、あくまで「指定時間後にいつか実行される」のであって「指定した時間ちょうどに実行される」のではけっしてない。

現在実行中の関数や、待ち行列の先にいる関数が重い処理を行なっていた場合、それだけコールバック関数の実行されるタイミングは遅くなる。

よって以下の場合、200ミリ秒後のどのタイミングでonSecond()が実行されるかは、その前に実行されるdoSample()、doFirst()しだい。

function doSample() {
	setTimeout(onFirst, 100);
	setTimeout(onSecond, 200);
	
	for (var i = 0; i < 1000; ++i) {
		// 重い処理
	}
	
	function onFirst() {
		// 重い処理
	}
	
	function onSecond() {
		alert("fin");
	}
}

doSample();

setInterval()、setTimeout()に指定したコールバック関数が実行されるタイミング
「指定時間が経ち、かつ、実行中および待ち行列の先にいる他の関数の処理が終了していること」

【本物のJavaScriptスレッド】

以上だけならば比較的わかりやすいのだが、最近は「Web Workers」という本物のスレッドが実装されはじめている。

こうなってくると最初に書いたように、マルチスレッドの難しさがからんでくる。

Web Workersを使う機会はまだ少ないだろうが、非同期処理とのからみが非常に難しくなることを覚悟しておこう。