随着计算机硬件的不断进步,要充分利用多核 CPU 才能让程序真正发挥高性能。多线程编程可以使得程序能够采用并行结构,加速运行速度。但是,多线程编程的实现一般需要借助于操作系统或者其他编程语言的库函数,而 JavaScript 作为一门单线程语言,一直无法在这方面提供强有力的支持。不过,ES8 的共享内存多线程功能的引入改变了这一现状,JavaScript 的多线程编程终于成为了可能。
共享内存多线程的概念
共享内存多线程,指的是多个线程共享同一块内存区域,通过访问这一块内存区域来进行线程之间的通信和同步。
在 JavaScript 中,使用共享内存多线程的最大好处就是提高程序的计算效率,而且还能保持 code readability。
共享内存多线程的基本用法
在 ES8 中,有一个全局对象 Atomics,提供了多线程编程中常用的操作方法。
Atomics 加减操作
Atomics.add(typedArray, index, value) // 原子加操作 Atomics.sub(typedArray, index, value) // 原子减操作 Atomics.compareExchange(typedArray, index, expectedValue, newValue) // 条件比较与替换操作
这些方法可以对 sharedArrayBuffer 的特定元素进行原子操作。
Atomics.wait 和 Atomics.wake
Atomics.wait(sharedArrayBuffer, index, value, timeout); // 使当前线程进入等待队列,等待共享内存的值发生变化 Atomics.wake(sharedArrayBuffer, index, count); // 从等待队列中唤醒指定数量的线程,使它们开始运行
这些方法可以通过让线程等待某个条件,保持 sharedArrayBuffer 中的值不变,当共享内存的值发生变化时,同时通知有关各方的变化。
共享内存多线程的换位理解
共享内存多线程,最关键的是不同线程间跨线程访问变量,数据不一致的问题。
换位理解,就是说明多个线程同时修改同一个数据时,为了协调各个线程来避免他们之间的竞争,在多个线程之间排队规则。
在 JavaScript 中,我们通过 Lock,Condition Variable 两种方法来使用多线程编程。
Lock
当一个线程访问共享数据时,就需要首先获得一个锁(或者称为 semsphore),只有获得锁的线程才能修改共享数据。
在 JavaScript 中,使用 Atomics.wait 和 Atomics.wake 方法控制锁,使用 Atomics.compareExchange 来保证读取和修改数据的原子性。
-- -------------------- ---- ------- --- ----------------- - --- --------------------- --- -------------------------------- - --- --- ------ - --- -------------------- -------------------------------------- ---------------- ------------------------------ -- ---- -- -------------- ----------------- ---
Condition Variables
Condition Variables 能够让一个线程等待某一个共享变量的值变成一个特定的值。当共享变量的值变成这个特定值时,这个线程被唤醒,然后这个线程可以重新获得一个锁来访问共享数据。
-- -------------------- ---- ------- --- ----------------- - --- --------------------- --- ------ - --- -------------------- -------------------------------------- ---------------- ------------------------------ -- ---- ----------------- ------------------------------ -- --- -- ---- - --- ----------------- --- ---------------- ------------------------------ -- --- -- --------
共享内存多线程的示例代码
下面是一个简单的示例,展示共享内存多线程的具体用法。
1. worker.js
worker.js 代码:
-- -------------------- ---- ------- --------- - --------------- - --- ----------------- - ----------- --- ---- - --- ------------------------------ --- --- - ----------------- -- --- ---------------------- - ------- - --------- -- ---- --- -- - ------------------- -- --- -------------------- -- --- -- ------------------ -- --- - -
其中 Atomics.add 方法会对 sharedArrayBuffer 中的第一个元素进行原子加 1 操作,并返回新值,这个过程是原子的。
当加 1 后新值为 3,则说明加满了,这时 worker.js 将 sharedArrayBuffer 中的第一个元素清零,然后将整个 sharedArrayBuffer 中第一个元素所在的等待队列中的条件变量的 waiters 中第一个线程唤醒。
2. script.js
script.js 代码:
-- -------------------- ---- ------- --- ----------------- - --- --------------------- --- ------- - --- -------------------- --- ------- - --- -------------------- --------------------------------------- --------------------------------------- ---------------- ------------------------------ -- --- -- ----- ----------------- -------- - ---------------------- - ------- - ----- ------------------------------------ ---------------- ------------------------------ -- --- -- -- ---------------------- - ------- - ----- ------------------------------------
这个过程的另外一个等待是先阻塞两个线程,然后等到 sharedArrayBuffer 中的第一个元素被 worker.js 库修改成 0 时 (Atomics.notify),这两个线程才将阻塞且等待结束,继续运行下面的代码。
代码运行完毕后,会输出共享数组 sharedArrayBuffer 中的第一个元素。sharedArrayBuffer 中的元素在 worker.js 运行过程中被修改,script.js 获取的值取决于 worker.js 的运行过程。
总结
ES8 的共享内存多线程功能,使得 JavaScript 能够支持多线程编程,这样开发者可以充分利用多核 CPU 的优势,大大提高程序的计算效率。通过使用 Atomics,我们可以实现对 sharedArrayBuffer 中的特定元素的原子访问,通过使用 Atomics.wait 和 Atomics.wake 来控制线程的等待和唤醒,以及通过使用 Lock 和 Condition Variables 来控制多个线程同时访问共享变量。
在开发 JavaScript 程序时,采用共享内存多线程编程技术,可比单线程编程有更好的执行效率,同时也会更加便于开发和调试。
来源:JavaScript中文网 ,转载请注明来源 https://www.javascriptcn.com/post/649d3d2448841e98949fad35