本文主要是介绍Node原子计数器,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 基础
- Automics
- Mutex
- 异常并发 case 非原子
- 正常操作 case 原子
基础
node 并发node通过单线程来处理高并发的请求。
一个事件循环中的执行是可以保证并发安全的,但是也业务操作并发读写一样会有业务的并发问题
在 JavaScript 中,函数总是运行到完成。这意味着如果一个函数正在运行,那么它将完全运行; 只有在这之后,才会调用另一个函数。因此,语句之间不存在交织的可能性(但是对于 Java 来说就不同了)。
单线程 eventLoop
线程锁:单线程编程模式下请求是顺序的,一个好处是不需要考虑线程安全、资源竞争问题,因此当你进行 Node.js 编程时,也不会去考虑线程安全问题。那么多线程编程模式下,例如 Java 你可能很熟悉一个词 synchronized,通常也是 Java 中解决并发编程最简单的一种方式,synchronized 可以保证在同一时刻仅有一个线程去执行某个方法或某块代码。
进程锁:一个服务部署于一台服务器,同时开启多个进程,Node.js 编程中为了利用操作系统资源,根据 CPU 的核心数可以开启多进程模式,这个时候如果对一个共享资源操作还是会遇到资源竞争问题,另外每一个进程都是相互独立的,拥有自己独立的内存空间。关于进程锁通过 Java 中的 synchronized 也很难去解决,synchronized 仅局限于在同一个 JVM 中有效。
分布式锁:一个服务无论是单线程还是多进程模式,当多机部署、处于分布式环境下对同一共享资源进行操作还是会面临同样的问题。此时就要去引入一个概念分布式锁。如下图所示,由于先读数据在通过业务逻辑修改之后进行 SET 操作,这并不是一个原子操作,当多个客户端对同一资源进行先读后写操作就会引发并发问题,这时就要引入分布式锁去解决,通常也是一个很广泛的解决方案。
分布式锁
Automics
原子性操作
Atomics.add() - JavaScript | MDN
Atomics in JavaScript - GeeksforGeeks
describe('autoMicNumberCount', () => {const counter = new Int32Array(new SharedArrayBuffer(4));/*** @description 任务数量++* @private*/async function handCurrentTaskAdd() {Atomics.add(counter, 0, 1);}/*** @description 任务数量--* @private*/async function handCurrentTaskSub() {Atomics.sub(counter, 0, 1);}it('autoMicNumberCount test', async () => {const tasks = [];for (let i = 0; i < 10000; i++) {tasks.push(handCurrentTaskAdd());}for (let i = 0; i < 9000; i++) {tasks.push(handCurrentTaskSub());}await Promise.all(tasks);expect(Atomics.load(counter, 0)).toBe(1000);});
});
Mutex
锁
private readonly mutex = new Mutex();private currentTaskComplete = 1;/*** @description 任务数量++* @private*/private async handCurrentTaskAdd() {await this.mutex.runExclusive(async () => {this.currentTaskComplete++;});}/*** @description 任务数量--* @private*/private async handCurrentTaskSub() {await this.mutex.runExclusive(async () => {this.currentTaskComplete--;});}
异常并发 case 非原子
describe('autoMicNumberCount', () => {let a = 1;async function one() {return 1;}async function example() {// 操作被分割成了多个步骤,并且由于await one();的存在,中间可能会插入其他操作,这就打破了原子性。console.log('Adding 1 to a');a += await one();// 修改 a++ 最终结果就是一致的}it('autoMicNumberCount test', async () => {console.log(`Start, a = ${a}`);Promise.all([example(),example(),example(),]).then(() => {console.log(`All done, a = ${a}`);});});
});
正常操作 case 原子
对于JavaScript而言,由于它是单线程的(至少在V8引擎中是这样),因此在没有显式使用异步或并发特性的情况下,函数中的操作通常被认为是原子性的。
下面操作测试结果都是正常的,不过这块代码对于性能也没有特别苛刻要求,自己对底层了解还是不太足够没有特别大的把握,使用Automics放心一点吧
describe('autoMicNumberCount', () => {let count = 0;/*** @description 任务数量++* @private*/function handCurrentTaskAdd() {count++;}/*** @description 任务数量--* @private*/function handCurrentTaskSub() {count--;}it('autoMicNumberCount test', async () => {const tasks = [];for (let index = 0; index < 10000; index++) {tasks.push(handCurrentTaskAdd());}for (let index = 0; index < 9000; index++) {tasks.push(handCurrentTaskSub());}await Promise.all(tasks);expect(count).toBe(1000);});
});
describe('autoMicNumberCount', () => {let count = 0;beforeEach(() => {// 在每个测试之前启用假定时器jest.useFakeTimers();});afterEach(() => {// 在每个测试之后恢复真实的定时器jest.useRealTimers();});/*** @description 任务数量++* @private*/async function handCurrentTaskAdd() {// 定时器延迟 1 毫秒执行setTimeout(() => {count++;}, 1);}/*** @description 任务数量--* @private*/async function handCurrentTaskSub() {setTimeout(() => {count--;}, 1);}it('autoMicNumberCount test', async () => {const tasks = [];for (let index = 0; index < 10000; index++) {tasks.push(handCurrentTaskAdd());}for (let index = 0; index < 9000; index++) {tasks.push(handCurrentTaskSub());}await Promise.all(tasks);// 使用 Jest 的 advanceTimersByTime 方法来推进时间jest.advanceTimersByTime(100); // 推进足够的时间以确保所有回调都已执行// 确保所有 setTimeout 回调都已经执行expect(count).toBe(1000);});
});
这篇关于Node原子计数器的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!