2

發揮 JavaScript 多執行緒威力 - Web Worker

 2 years ago
source link: https://blog.darkthread.net/blog/web-worker/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
發揮 JavaScript 多執行緒威力

前陣子討論到 Chrome 88+ 加入的 Intensive Throttling 會造成 setInterval 大走鐘,同事提到可用 Worker 避開問題,發文後讀者 Scott 也留言提到。不過,在這個議題上,我覺得搬進 Worker 規避 Chrome 節能設計雖然省事但非最佳解,若有其他更有效率的替代方案,改用更省 CPU 省電的寫法是上策。

但這倒提醒我,自己從沒用過 Web Worker,不清楚其優勢及應用時機,趁此機會補上吧。

Web Worker 已經推出多年,不是什麼新技術,連 IE10 都支援即可見一斑,網路上教學與討論不少,我主要參考阮一峰老師的這篇 - Web Worker 使用教程,寫得很淺白完整,這篇文章會跳過 Web Worker 基本教學,直接來看 Web Worker 的優點及應用場合。

首先,要先確立一個觀念,網頁裡的 JavaScript 一直是用同一條執行緒執行,即便 setTimeout/setInterval 設定完就能跑其他程式,時間到會自動執行指定的函式,給人多執行緒並行的錯覺。事實上,setTimeout/setInterval 執行時會中斷原本執行中的 JavaScript 指令,等其結束後再繼續。Web Worker 的好處即在於它在獨立的背景執行緒執行,不會跟網頁裡的 JavaScript 搶 CPU,這便是我想藉由實驗驗證的點。

要突顯 Web Worker 多執行緒的優勢,要找個網頁 JavaScript 很忙的範例,我找到很吃 CPU 的粒子移動模擬 JavaScript Canvas 效能測試

至於同時要跑的重量級運算,我很沒創意地選擇計算 0 ~ n 包含的質數,第一個版本用 setTimeout 來做:

<!DOCTYPE html>
<html>

<head>
	<title>Benchmark - setTimeout</title>
	<script src="excanvas.compiled.js"></script>
	<script src="animation.js"></script>
	<style>
		#info {
			position: absolute;
			z-index: 99;
			color:yellow;
			font-size: 14pt;
			top: 10px;
			left: 10px;
			padding: 6px;
			background-color: gray;
		}
	</style>
</head>

<body>
	<div id='benchmark' style='width: 800px; height: 400px'></div>
	<div id=info>
	</div>
	<script>
		let info = document.getElementById('info');
		function isPrime(num) {
			if (num <= 3) return num > 1;
			if ((num % 2 === 0) || (num % 3 === 0)) return false;
			let count = 5;
			while (Math.pow(count, 2) <= num) {
				if (num % count === 0 || num % (count + 2) === 0) return false;
				count += 6;
			}
			return true;
		}
		function listPrimes(maxNum) {
			let res = [];
			for (let i = 1; i <= maxNum; i++) {
				if (isPrime(i)) res.push(i);
			}
			return res;
		}
		var st = new Date().getTime();
		setTimeout(function () {
			let res = listPrimes(10000000);
			dura = (new Date().getTime() - st);
			document.getElementById('info').innerText += dura + 'ms ' + res.length + ' primes';
		}, 1000);
	</script>
</body>

</html>

計算質數部分我刻意延遲一秒開始,用意讓粒子動晝先播放一陣子,一秒後透過 setTimeout 執行尋找 1 ~ 1000 萬所含質數,如包含一秒延遲,共耗時 3.3s。

接著來改寫 Web Worker 版,寫一支 worker.js:

function isPrime(num) {
    if (num <= 3) return num > 1;
    if ((num % 2 === 0) || (num % 3 === 0)) return false;
    let count = 5;
    while (Math.pow(count, 2) <= num) {
        if (num % count === 0 || num % (count + 2) === 0) return false;
        count += 6;
    }
    return true;
}
function listPrimes(maxNum) {
    let res = [];
    for (let i = 1; i <= maxNum; i++) {
        if (isPrime(i)) res.push(i);
    }
    return res;
}
self.addEventListener('message', function (e) {
    var res = listPrimes(e.data);
    console.log('done');
    self.postMessage(res);   
    self.close(); 
}, false);

網頁程式部分一樣先跑動晝,setTimeout 延遲一秒啟動 Worker 尋找 1 ~ 1000 萬所含質數:

<!DOCTYPE html>
<html>

<head>
	<title>Benchmark - Worker</title>
	<script src="excanvas.compiled.js"></script>
	<script src="animation.js"></script>
	<style>
		#info {
			position: absolute;
			z-index: 99;
			color:yellow;
			font-size: 14pt;
			top: 10px;
			left: 10px;
			padding: 6px;
			background-color: gray;
		}
	</style>
</head>

<body>
	<div class="snowflakes">
	</div>
	<div id=info>
	</div>
	<script>
		var worker = new Worker('worker.js');
		worker.onmessage = function(e) {
			let dura = (new Date().getTime() - st);
			document.getElementById('info').innerText = dura + 'ms ' + e.data.length + ' primes';
			console.log(new Date());
		}
		var st = new Date().getTime();
		setTimeout(() => {
			worker.postMessage(10000000);			
		}, 1000);
	</script>
</body>

</html>

Web Wroker 版,包含 1 秒延遲啟動耗時約 3.5s,咦?怎麼比 setTimout 還慢?建立 Worker 物件及 postMessage 溝通會耗用一些額外資源,但用 Worker 不就為了效能,反而更慢像話嗎?

其實上面我賣了個關子,如果實際看過兩者的執行結果,大家就會知道為什麼該用 Web Worker 了。

左邊的 setTimeout 版本,質數計算期間動晝完全凍結,算完才繼續,且因執行間隔錯亂,粒子原本應隨機亂跑,一度出現整群同步移動。而 Web Wroker 版,全程動畫順暢未受干擾,證明質數計算是用另一個執行緒在跑,不中斷網頁的 JavaScript 執行,真正實踐了多執行緒。

最後再補充另一個實驗,瀏覽器本身是多執行緒環境,受單一執行緒限制的是網頁的 JavaScript 程式,其他如 Render、CSS 等運算等作業,瀏覽器會安排不同執行緒處理。因此,如果今天是用純 CSS 製作的動晝(我找到一個雪花飄效果當範例),用 setTimeout 或 Web Worker 的差異不大。(註:測量結果未包含一秒延遲,故比之前少一秒)

從以上實驗,我的結論是 - 對於等待外部回應或短短幾行的簡單作業,用 Web Worker 有點殺雞用牛刀,寫成 setTimeout/setInterval 就夠了,程式還比較簡潔;但如果是會持續數秒以上的重度運算,若不想在計算過程畫面卡死凍結,便可借重 Web Worker 發揮多執行緒威力,提供更好的使用者體驗。

想動手玩看看的同學,線上版本在這裡:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK