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);
});
});