js是单线程执行的,很容易被阻塞,所以主线程在执行的时候,web Worker可以在后台开启一个子线程执行一些代码而不阻塞主线程的执行,子线程完全受主线程控制。
那什么情况下代码应该放在子线程里呢?比如:遇到计算量比较耗时的时候,将这部分代码放入子线程进行执行,再将执行结果抛给主线程,(不能让主线程一直等待这段代码执行完毕才进行下一步,如果耗时很长,页面会一直卡主)
Worker
先看一个关于Worker的小例子
html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>web Worker</title>
<script src='./main.js'></script>
</head>
<body>
<p>Count from w1: <span id="result"></span></p>
<button onclick="startWorker()">Start Worker</button>
<button onclick="stopWorker()">Stop Worker</button>
<input type="text">
</body>
</html>
main.js
// 以下是主线程代码
// 主线程通过onmessage方法接收来自worker线程的信息
// 也可通过postMessage()方法给worker线程传递信息
// terminate()方法立即终止 worker。该方法不会给 worker 留下任何完成操作的机会;就是简单的立即停止。
let w;
let result = document.getElementById("result");
function startWorker(){
if(typeof Worker === 'undefined'){
result.innerHTML = '您的浏览器不支持Web Worker!';
}else{
if(typeof w === 'undefined'){
w = new Worker('./w1.js');
}
w.onmessage = function(e){
result.innerHTML = e.data;
}
}
}
function stopWorker(){
w.terminate();
}
w1.js文件
// 同样worker线程也可通过postMessage()方法给主线程传递信息
function w1(){
for(let i = 0; i < 100000000000000000000000; i++){
if(i % 1000000 === 0){
postMessage(i);
}
}
}
w1();
点击开始后,数字一直在变(子线程里执行的),此时,在输入框里还能顺利输入内容(主线程里进行的操作),页面并没有被阻塞,说明子线程里执行的代码并没有阻塞主线程
如果遇到控制台报错
SharedArrayBuffer
既然主线程与子线程之间可以相互交换传递信息,那都可以传递哪些信息呢?答案是:可以是各种格式,不仅仅是字符串,也可以是二进制数据。这种交换采用的是复制机制,即一个线程将需要分享的数据复制一份,通过postMessage方法交给另一个线程。如果数据量比较大,这种通信的效率显然比较低。因此就会想到,如果开辟一个空间读写信息,但是只传递这个空间的引用地址,岂不是会很好解决这个问题,所以SharedArrayBuffer就应运而生,顾名思义,共享的内存,我们用一个小例子来看一下这个怎么使用。
// 我们先来说一下使用流程
// 首先要新建共享内存 const sharedBuffer = new SharedArrayBuffer(size) // size 指内存空间的大小 以byte字节为单位 size = 1024 即 1KB
// 其次要在指定的内存上建立视图以供读写数据 const ia = new Int32Array(sharedBuffer) // 因为内存本身是不具有读写能力的
// 假如我这里是主线程 main.js
// 创建一个worker线程
const wk = new Worker('./w1.js');
// 建立 1KB 内存
const sharedBuffer = new SharedArrayBuffer(1024);
// 建立视图
const ia = new Int32Array(sharedBuffer);
// console.log(ia)
// 向内存中写入数据
for(let i = 0; i < ia.length; i++){
ia[i] = i;
}
// 将内存地址发送给worker线程
wk.postMessage(sharedBuffer);
// 从子线程获取修改后的信息
wk.onmessage = function(e){
console.log(`修改后的数据`,ia[20])
}
// 这里是子线程 w1.js
// onmessage从主线程接收信息
onmessage = function(e){
// 接收内存地址
const sharedBuffer = e.data;
// console.log(sharedBuffer)
// 同样的 子线程要读写内存也需要建立视图
const ia = new Int32Array(sharedBuffer);
// worker线程修改内存里某一项 主线程再读取的时候就会看到修改后的数据
ia[20] = 999;
// 为了使主线程能获取到修改后的内存地址 只要用postMessage()触发主线程的onmessage即可
postMessage('已修改内存数据');
}
Atomics
看到这心里不禁会问,既然是共享的内存,那多个线程同时修改同一个内存的时候会不会出现问题,这个时候就出现竞争问题了,为了防止多个线程同时修改某个地址,或者说,当一个线程修改共享内存以后,必须有一个机制让其他线程同步。SharedArrayBuffer API 提供Atomics对象,保证所有共享内存的操作都是“原子性”的,并且可以在所有线程内同步。
什么叫“原子性操作”呢?现代编程语言中,一条普通的命令被编译器处理以后,会变成多条机器指令。如果是单线程运行,这是没有问题的;多线程环境并且共享内存时,就会出问题,因为这一组机器指令的运行期间,可能会插入其他线程的指令,从而导致运行结果出错。看个例子
// 主线程
ia[42] = 314159; // 原先的值 191
ia[37] = 123456; // 原先的值 163
// Worker 线程
console.log(ia[37]);
console.log(ia[42]);
// 可能的结果
// 123456
// 191
上面代码中,主线程的原始顺序是先对 42 号位置赋值,再对 37 号位置赋值。但是,编译器和 CPU 为了优化,可能会改变这两个操作的执行顺序(因为它们之间互不依赖),先对 37 号位置赋值,再对 42 号位置赋值。而执行到一半的时候,Worker 线程可能就会来读取数据,导致打印出123456和191。
下面是另一个例子。
// 主线程
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);
const ia = new Int32Array(sab);
for (let i = 0; i < ia.length; i++) {
ia[i] = primes.next(); // 将质数放入 ia
}
// worker 线程
ia[112]++; // 错误
Atomics.add(ia, 112, 1); // 正确
上面代码中,Worker 线程直接改写共享内存ia[112]++是不正确的。因为这行语句会被编译成多条机器指令,这些指令之间无法保证不会插入其他进程的指令。请设想如果两个线程同时ia[112]++,很可能它们得到的结果都是不正确的。
Atomics对象就是为了解决这个问题而提出,它可以保证一个操作所对应的多条机器指令,一定是作为一个整体运行的,中间不会被打断。也就是说,它所涉及的操作都可以看作是原子性的单操作,这可以避免线程竞争,提高多线程共享内存时的操作安全。所以,ia[112]++要改写成Atomics.add(ia, 112, 1)。
所以上面对 ia 正确的修改方法应该是 Atomics.store(ia, 20, 999);
接下来看一下Atomics都有哪些可操作方法
// 比起直接的读写操作,它们的好处是保证了读写操作的原子性。
Atomics.load(ia, index)
// 用来从共享内存读出数据
// ia 对象(SharedArrayBuffer 的视图)
// index 位置索引
// 返回ia[index]的值
Atomics.store(ia, index, value)
// 用来向共享内存写入数据
// ia 对象(SharedArrayBuffer 的视图)
// 位置索引
// 值
// 返回ia[index]的值
Atomics.exchange(ia, index, value)
// 用来向共享内存写入数据 与 Atomics.store()的区别是 Atomics.store()返回写入的值,而Atomics.exchange()返回被替换的值
// ia 对象(SharedArrayBuffer 的视图)
// 位置索引
// 值
// 返回ia[index]原来的值(被替换之前的值)
Atomics.wait(ia, index, value, timeout)
// 使线程进入休眠 满足条件才进入休眠 此方法一般用于worker线程 注意,浏览器的主线程不宜设置休眠,这会导致用户失去响应。而且,主线程实际上会拒绝进入休眠。
// ia:共享内存的视图数组。
// index:视图数据的位置(从0开始)。
// value:该位置的预期值。一旦实际值等于预期值,就进入休眠。
// timeout:整数,表示过了这个时间以后,就自动唤醒,单位毫秒。该参数可选,默认值是Infinity,即无限期的休眠,只有通过Atomics.notify()方法才能唤醒
// Atomics.wait()的返回值是一个字符串,共有三种可能的值。如果ia[index]不等于value,就返回字符串not-equal,否则就进入休眠。如果Atomics.notify()方法唤醒,就返回字符串ok;如果因为超时唤醒,就返回字符串timed-out。
// 例如: Atomics.wait(ia, 20, 33, 3000) 解释:当 20 这个位置的值等于33的时候,所在线程才进入休眠,3秒之后自动唤醒,如果没有 3000 这个参数,只有通过Atomics.notify()方法才能唤醒
Atomics.notify(ia, index, count)
// ia:共享内存的视图数组。 此方法一般用于主线程
// index:视图数据的位置(从0开始)。
// count:需要唤醒的 Worker 线程的数量,默认为Infinity
// 运算方法
// 共享内存上面的某些运算是不能被打断的,即不能在运算过程中,让其他线程改写内存上面的值。Atomics 对象提供了一些运算方法,防止数据被改写。
Atomics.add(ia, index, value)
// Atomics.add用于将value加到ia[index],返回ia[index]旧的值。
Atomics.sub(ia, index, value)
// Atomics.sub用于将value从ia[index]减去,返回ia[index]旧的值。
// 位运算方法
Atomics.and(ia, index, value)
Atomics.or(ia, index, value)
Atomics.xor(ia, index, value)
// 以上三种方法都是用于将vaule与ia[index]进行位运算and/or/xor,放入ia[index],并返回旧的值。
// Atomics对象还有以下方法。
Atomics.compareExchange(ia, index, oldval, newval)
// 如果ia[index]等于oldval,就写入newval,返回oldval。
// Atomics.compareExchange的一个用途是,从 SharedArrayBuffer 读取一个值,然后对该值进行某个操作,操作结束以后,检查一下 SharedArrayBuffer 里面原来那个值是否发生变化(即被其他线程改写过)。如果没有改写过,就将它写回原来的位置,否则读取新的值,再重头进行一次操作。
Atomics.isLockFree(size)
// 返回一个布尔值,表示Atomics对象是否可以处理某个size的内存锁定。如果返回false,应用程序就需要自己来实现锁定。