web Worker -- SharedArrayBuffer -- Atomics

16 篇文章 0 订阅

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,应用程序就需要自己来实现锁定。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值