从sleep的实现说起

在nodejs中,如果要实现sleep的功能主要是通过“setTimeout + promise”实现,也可以通过“循环空转”来解决。前者是利用定时器实现任务的延迟执行,并通过promise链管理任务间的时序与依赖,本质上nodejs的执行线程并没有真正的sleep,事件循环以及v8仍在运行,是仅仅表现在业务逻辑上sleep;而后者的实现则无疑实在浪费CPU性能,有点类似自旋锁,不符合大多数场景。

若要实现引擎层面(运行时)的sleep,事情在ECMAScript Latest Draft (ECMA-262)出现之后开始有了转机。ECMA262规定了 Atomics.wait,它会将调用该方法的代理(引擎)陷入等待队列并让其sleep,直到被notify或者超时。该规范在8.10.0以上版本的nodejs上被实现。

事实上,Atomics.wait 的出现主要解决浏览器或nodejs的worker之间数据同步的问题。浏览器上的web-worker、正式被nodejs@12纳入的worker-threads模块,这些都是ECMAScript多线程模型的具体实现。既然出现多线程那么线程间的同步也就不可避免的被提到,在前端以及nodejs范围内可以使用Atomics.wait和notify来解决。

说的有些跑题,回到本节,如何实现运行时的sleep呢?很简单,利用Atomics.wait的等待超时机制:

let sharedBuf = new SharedArrayBuffer(4);
let sharedArr = new Int32Array(sharedBuf);
// 睡眠n秒
let sleep = function(n){
Atomics.wait(sharedArr, 0, 0, n * 1000);
}

此处的sleep并不是异步方法,它会阻塞执行线程直到超时,因此需要根据业务场景来使用该sleep模型。

关于Atomics.wait的具体使用方法,下文会着重讲解。

多线程同步

虽然nodejs多线程使用场景不是很多,但是一旦涉及到多线程,那么线程间同步就必不可少,否则无法解决临界区的问题。不过nodejs的work_threads对线程的创建不同于c或者java,它使用libuv的API创建线程 “uv_thread_create”,但是在此之前需要初始化一些设施如MessagePort、v8实例设置等,因此创建一个thread并不是一个轻量级的操作,需要结合场景酌情创建适量的threads。

回到正题,多线程间的同步一般需要依赖锁,而锁的实现需要依赖于全局变量。在nodejs的work_threads实现中,主线程无法设置全局变量,因此可以通过Atomics实现。正如上例中所示,Atomics.wait依赖 SharedArrayBuffer,这是共享内存的ArrayBuffer,threads之间可通过它共享数据,可真正操作ArrayBuffer时并不直接使用该对象,而是TypeArray。如Atomics.wait,第一个参数必须是Int32Array对象,而该对象指向的缓冲区为SharedArrayBuffer。当线程A因为Atomics.wait而阻塞后,可通过其它线程B调用Atomics.notify进行唤醒从而让线程A的v8继续执行。

let { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
var sab = new SharedArrayBuffer(1024);
var int32 = new Int32Array(sab);
if (isMainThread) {
const worker = new Worker(__filename, {
workerData: sab
});
worker.on('message', (d) => {
console.log('parent receive message:', d);
});
worker.on('error', (e) => {
console.error('parent receive error', e);
});
worker.on('exit', (code) => {
if (code !== 0)
console.error(new Error(`工作线程使用退出码 ${code} 停止`));
}); Atomics.wait(int32, 0, 0); // A
console.log(int32[0]); // C: 123
} else {
let buf = workerData;
let arrs = new Int32Array(buf);
Atomics.store(arrs, 0, 123);
Atomics.notify(arrs, 0); // B
}

上例中,主线程创建thread后,在A处进行阻塞;在新线程中,通过原子操作Atomics.store修改SharedArrayBuffer的第一项为123后,于B处唤醒阻塞在SharedArrayBuffer第一项的其它线程;此时主线程被唤醒,执行console.log(int32[0]),输出被新线程修改后的SharedArrayBuffer第一项数据123。

分析一个公平、排它、不可重入锁的实现,它使用Atomics.wait/notify/compareExchange完成线程的同步。

main-thread.js

let  Lock  =  require('./lock').Lock;
let { Worker } = require('worker_threads');
const sharedBuffer = new SharedArrayBuffer(1 * Int32Array.BYTES_PER_ELEMENT);
const sharedArray = new Int32Array(sharedBuffer);
let worker = new Worker('./worker-lock.js', {
workerData: sharedBuffer
});
Lock.initialize(sharedArray, 0);
const lock = new Lock(sharedArray, 0);
// 获取锁
lock.lock(); // 3s后释放锁
setTimeout(() => {
lock.unlock(); // (B)
}, 3000)
worker-thread.js

let  Lock  =  require('./lock').Lock;
let { parentPort, workerData } = require('worker_threads');
const sharedArray = new Int32Array(workerData);
const lock = new Lock(sharedArray, 0); console.log('Waiting for lock...'); // (A)
// 获取锁
lock.lock(); // (B) blocks!
console.log('Unlocked'); // (C)

主线程初始化互斥锁,同时创建线程,主线程获取锁后三秒钟释放;

worker线程尝试获取锁,此时锁已被主线程获取,因此worker线程在此阻塞,等待3s后主线程释放锁被唤醒,继续执行输出。

lock.js

const  UNLOCKED  =  0;
const LOCKED_NO_WAITERS = 1;
const LOCKED_POSSIBLE_WAITERS = 2;
const NUMINTS = 1; class Lock {
// 'iab' must be a Int32Array mapping shared memory.
// 'ibase' must be a valid index in iab, the first of NUMINTS reserved for the lock.
constructor(iab, ibase) {
if (!(iab instanceof Int32Array && ibase|0 === ibase && ibase >= 0 && ibase+NUMINTS <= iab.length)) {
throw new Error(`Bad arguments to Lock constructor: ${iab} ${ibase}`);
}
this.iab = iab;
this.ibase = ibase;
}
static initialize(iab, ibase) {
if (!(iab instanceof Int32Array && ibase|0 === ibase && ibase >= 0 && ibase+NUMINTS <= iab.length)) {
throw new Error(`Bad arguments to Lock constructor: ${iab} ${ibase}`);
}
Atomics.store(iab, ibase, UNLOCKED);
return ibase;
}
// Acquire the lock, or block until we can. Locking is not recursive:
lock() {
const iab = this.iab;
const stateIdx = this.ibase;
var c;
if ((c = Atomics.compareExchange(iab, stateIdx, UNLOCKED, LOCKED_NO_WAITERS)) !== UNLOCKED) { // A
do {
if (c === LOCKED_POSSIBLE_WAITERS
|| Atomics.compareExchange(iab, stateIdx, LOCKED_NO_WAITERS, LOCKED_POSSIBLE_WAITERS) !== UNLOCKED) {
Atomics.wait(iab, stateIdx, LOCKED_POSSIBLE_WAITERS, Number.POSITIVE_INFINITY);
}
} while ((c = Atomics.compareExchange(iab, stateIdx, UNLOCKED, LOCKED_POSSIBLE_WAITERS)) !== UNLOCKED); // B
}
}
tryLock() {
const iab = this.iab;
const stateIdx = this.ibase;
return Atomics.compareExchange(iab, stateIdx, UNLOCKED, LOCKED_NO_WAITERS) === UNLOCKED;
}
unlock() {
const iab = this.iab;
const stateIdx = this.ibase;
var v0 = Atomics.sub(iab, stateIdx, 1);
// Wake up a waiter if there are any
if (v0 !== LOCKED_NO_WAITERS) {
Atomics.store(iab, stateIdx, UNLOCKED);
Atomics.notify(iab, stateIdx, 1);
}
}
toString() {
return "Lock:{ibase:" + this.ibase +"}";
}
}
exports.Lock = Lock;

当进程A尝试获取锁成功时,A处判断语句为false,因此由compareExchange设置状态为LOCKED_NO_WAITERS,直接执行其后续逻辑;

若进程B此时执行lock获取锁时,A处判断为true,进入do while循环体,在wait处sleep;

进程A通过unlock释放锁,会将锁状态置为UNLOCKED,同时唤醒阻塞的进程B;

进程B执行循环判断语句B,此时为false,跳出循环执行B的逻辑。

当然,也可通过tryLock实现自旋锁或者其他逻辑实现非阻塞等待。

参考

libuv漫谈之线程

Atomics

Atomics MDN

nodejs中的并发编程的更多相关文章

  1. Python中的并发编程

    简介 我们将一个正在运行的程序称为进程.每个进程都有它自己的系统状态,包含内存状态.打开文件列表.追踪指令执行情况的程序指针以及一个保存局部变量的调用栈.通常情况下,一个进程依照一个单序列控制流顺序执 ...

  2. [翻译]在 .NET Core 中的并发编程

    原文地址:http://www.dotnetcurry.com/dotnet/1360/concurrent-programming-dotnet-core 今天我们购买的每台电脑都有一个多核心的 C ...

  3. .NET Core 中的并发编程

    今天我们购买的每台电脑都有一个多核心的 CPU,允许它并行执行多个指令.操作系统通过将进程调度到不同的内核来发挥这个结构的优点. 然而,还可以通过异步 I/O 操作和并行处理来帮助我们提高单个应用程序 ...

  4. Go中的并发编程和goroutine

    并发编程对于任何语言来说都不是一件简单的事情.Go在设计之初主打高并发,为使用者提供了goroutine,使用的方式虽然简单,但是用好却不是那么容易,我们一起来学习Go中的并发编程. 1. 并行和并发 ...

  5. Go语言中的并发编程

    并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因. Go语言中的并发编程 并发与并行 并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天) ...

  6. Java中的并发编程集合使用

    一.熟悉Java自带的并发编程集合 在java.util.concurrent包里有很多并发编程的常用工具类. package com.ietree.basicskill.mutilthread.co ...

  7. 深入理解nodejs中的异步编程

    目录 简介 同步异步和阻塞非阻塞 javascript中的回调 回调函数的错误处理 回调地狱 ES6中的Promise 什么是Promise Promise的特点 Promise的优点 Promise ...

  8. C#中的并发编程知识二

      = 导航   顶部 基本信息 ConcurrentQueue ConcurrentStack ConcurrentBag BlockingCollection ConcurrentDictiona ...

  9. C#中的并发编程知识

      = 导航   顶部 线程 Task async/await IAsyncResult Parallel 异步的回调   顶部 线程 Task async/await IAsyncResult Pa ...

随机推荐

  1. django反向解析和正向解析

    Django的正向解析和反向解析 先创建一个视图界面 urls.py index.html index页面加载的效果 正向解析 test/?result=1 所谓正向解析就是直接在这里写地址 向url ...

  2. 整数拆分-dp问题

    Integer Partition In number theory and combinatorics, a partition of a positive integer n, also call ...

  3. Docker学习笔记_10 docker应用 - 部署TOMCAT服务

    选择基镜像 基镜像使用dokcer hub官方提供的tomcat8 alpine当前最新版本,https://hub.docker.com/_/tomcat/ docker pull tomcat:8 ...

  4. java 实体类中日期格式转换

    @JsonFormat(locale="zh", timezone="GMT+8", pattern="yyyy-MM-dd HH:mm:ss&quo ...

  5. Css兼容性大全

    知识有所欠缺  疯狂脑补抄袭经验中... 兼容性处理要点1.DOCTYPE 影响 CSS 处理 2.FF: 设置 padding 后, div 会增加 height 和 width, 但 IE 不会, ...

  6. TensorFlow_Faster_RCNN中demo.py的运行(CPU Only)

    GitHub项目地址,https://github.com/endernewton/tf-faster-rcnnTensorflow Faster RCNN for Object Detection. ...

  7. 公司更需要会哪种语言的工程师?​IEEE Spectrum榜单发布

    IEEE Spectrum 杂志发布了一年一度的编程语言排行榜,这也是他们发布的第四届编程语言 Top 榜. 据介绍,IEEE Spectrum 的排序是来自 10 个重要线上数据源的综合,例如 St ...

  8. Visual Studio 2013编译Tesseract 3.04

    文章目录 去年时候使用了VS2008编译了Tesseract 3.02版本,主要是参考了一份官方文档,但是对于目前的最新版本并没有给出说明. 本文主要参考了Paul Vorbach的How to bu ...

  9. 推荐一款在UBUNTU下使用的编辑器

    偶然的机会 ,发现了这款软件,以前一直是在用gedit编辑器,但在WINDOWS下写的文档,在ubuntu下用gedit打开后,复制有换行的问题,一直没解决,所以在网上找到了这款软件,安装使用了几天, ...

  10. 分布式系统一致性问题与Raft算法(上)

    最近在做MIT6.824的几个实验,真心觉得每一个做分布式相关开发的程序员都应该去刷一遍(裂墙推荐),肯定能够提高自己的技术认知水平,同时也非常感谢MIT能够把这么好的资源分享出来. 其中第二个实验, ...