从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. sm4算法(附源码、测试代码)

    from:http://blog.csdn.net/mao0514/article/details/52930944 SM4是我们自己国家的一个分组密码算法,是国家密码管理局于2012年发布的.网址戳 ...

  2. 转-web自动化测试,定位不到元素的原因及解决方案

    1.动态id定位不到元素 分析原因:每次打开页面,ID都会变化.用ID去找元素,每次刷新页面ID都会发生变化. 解决方案:推荐使用xpath的相对路径方法或者cssSelector查找到该元素. 2. ...

  3. ASP.NET MVC4网站搭建与发布【最新】

    ASP.NET MVC4网站搭建与发布 一些往事 2015年,仅仅大二的我怀着一颗创业之心,加入了常熟派英特,成为阳光职场平台的创始之一,并肩负了公司技术部的大梁,当时阳光职场正在从线下服务向互联网化 ...

  4. xpath_note - Ethan Lee

    https://ethan2lee.github.io/ XPath概览 XPath,全称XML Path Language,即XML路径语言,它是一门在XML文档中查找信息的语言.它最初是用来搜寻X ...

  5. mysql关系型数据库

    参考:https://www.cnblogs.com/alex3714/articles/5950372.html 关系型数据库,是建立在关系模型基础上的数据库,借助于集合代数等数学概念和方法来处理数 ...

  6. TesterHome创始人思寒:如何从手工测试进阶自动化测试?十余年经验分享

      做测试十多年,有不少人问过我下面问题: 现在的手工测试真的不行了吗? 测试工程师,三年多快四年的经验,入门自动化测试需要多久? 自学自动化测试到底需要学哪些东西? 不得不说,随着行业的竞争加剧,互 ...

  7. 从TP-Link到雷蛇,纷纷入局智能手机业到底想干什么?

    ​   "眼看他起朱楼,眼看他宴宾客,眼看他楼塌了",这句形容世态炎凉的话其实与智能手机市场更为相像.诺基亚的辉煌与没落.黑莓的强势与消声无迹.摩托罗拉的数次易手.小米的横空出世与 ...

  8. 软件测试价值观-SMBT新理念

    软件测试价值观-SMBT新理念 作者:张元礼 http://blog.csdn.net/vincetest 近年来有不少软件测试同行不少有些困惑-软件测试人员的价值在哪里?我们怎么才能做好软件测试?怎 ...

  9. ypoj 2286 佳佳买菜

    题目名称:佳佳买菜 描述 佳佳是我们的ACM社团的副社长,她感觉得自己没存在感,so-由于实验室要聚餐了,佳佳决定买点菜,来做菜给大家吃.佳佳喜欢吃娃娃菜,于是她来到买菜的地方.佳佳:我要10斤娃娃菜 ...

  10. 「NOIP 2017」列队

    题目大意:给定一个 $n times m$ 的方阵,初始时第 $i$ 行第 $j$ 列的人的编号为 $(i-1) times m + j$,$q$ 次给出 $x,y$,让第 $x$ 行 $y$ 列的人 ...