![](https://img.haomeiwen.com/i17741696/6104155f139de7ef.png)
单线程天然的优势
nodejs 单线程, 意味在执行cpu操作的时候 不会切出,这样就可以保证一些代码执行的 原子性。
而多线程在对同一个对象进行写操作时,会出现问题,以下先用一个例子 ,解释下多线程无锁写操作存在潜在问题,
多线程的问题
以java举例,多线程操作同一个对象, 做个counter++的操作,如果不加锁,则会出现问题,
示例代码:
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 创建 1000 个线程,每个线程都会增加计数器的值
Thread[] threads = new Thread[1000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
// 等待所有线程完成
for (Thread thread : threads) {
thread.join();
}
// 输出计数器的值
System.out.println(counter.getCount());
}
}
执行
java main.java
输出
// 预期 1000000
// 实际输出:
994924
原因:
count++
实际上是一个复合操作,包括三个步骤:读取 count
的值,增加 count
的值,然后写回新的值。当多个线程同时执行这个操作时,它们可能会读取到相同的 count
值,然后都增加这个值,最后写回新的值。这就导致了一些增加操作被覆盖,因此最后的结果小于预期的 1000000。
对于单线程,则不会出现此问题,nodejs 基本都是单线程处理。那么nodejs 是不是就完全不需要使用锁了呢?
并不是的。确实nodejs在某些场景下不需要锁,但某些场景下是需要锁的,下面用不需要锁 和 需要锁的例子来进行下具体展示,继续👇🏻
什么时候需要锁,什么时候不需要。
我认为 在单线程中使用锁的一个关键,就是看代码在什么时候切出,即不连续的执行了,切出执行其它操作,存在这种切换的场景 并且对同一个对象进行操作时, 则需要锁。
对于nodejs中的单线程,连续的cpu运算是不会让出的,有io的时候 ,才会让出 ,所以在有io操作时,才有可能用到锁。
接下来,针对提到的需要锁 和 不需要锁,分别看下具体的示例,
不需要锁的情况
cpu计算,不切出,
举例: cpu-logic-no-lock.js
let counter = 0;
// 不需要锁的情况
function incrementCounter() {
counter++;
console.log(`Counter: ${counter}`);
}
// 模拟异步操作
async function simulateAsyncOperation(fn) {
// 同时启动10w个定时器,对counter进行累加
for (let i = 0; i < 100000; i++) {
setTimeout(fn, 0);
}
}
simulateAsyncOperation(incrementCounter);
执行
node cpu-logic-no-lock.js
结果
Counter 1
Counter: 2
Counter: 3
...
Counter: 100000
可以看出结果没有出现如上面java多线程情况下,造成增加操作被覆盖,从而小于100000的问题。
原因:counter++;这句话执行的时候 ,不会被打断,从而不会出现上述的问题,换句话说 ,单线程 cpu运算是不会切出的。
需要锁的情况
而io的操作,会切出
举例:io-logic-need-lock.js
// 需要锁的情况
async function incrementCounter() {
// 伪代码
// io操作,从数据库读取某个值累加.
// 通过mongodb.counter.query()获取counter的值
let counter = await mongodb.counter.findOne({});
counter.n++;
// 通过mongodb.counter.update()更新counter的值
await mongodb.counter.updateOne(
{ _id: counter._id }, { $set: { n: counter.n } }
);
console.log(`Counter: ${counter.n}`);
}
// 模拟异步操作
async function simulateAsyncOperation(fn) {
for (let i = 0; i < 100000; i++) {
// 同时启动10w个定时器
setTimeout(fn, 0);
}
}
// 需要锁的情况
simulateAsyncOperation(incrementCounter);
伪代码解释:上述的counter是从数据库中读取,也就存在了io的操作,当读取数据时,碰到 await mongodb.counter.findOne({}); 等语句,则会让出,
结果:
// 小于10w
994123
原因:
![](https://img.haomeiwen.com/i17741696/61719fe51b428689.png)
同时处理时, 可以看出 由于遇到io操作时,会让出,则会出现如 右2 在左1 未完成累加的情况下拿到了相同的counter,从而造成增加操作被覆盖。那么应该如何调整? 一种方式便是加锁 。
锁的示例
const { Mutex } = require('async-mutex');
const mutex = new Mutex();
// 需要锁的情况
async function incrementCounterWithLock() {
// 这里加锁
const release = await mutex.acquire();
try {
let counter = await mongodb.counter.findOne({});
counter.n++;
await mongodb.counter.updateOne(
{ _id: counter._id }, { $set: { n: counter.n } }
);
console.log(`Counter: ${counter.n}`);
}
finally {
release();
}
}
// 模拟异步操作
async function simulateAsyncOperation(fn) {
for (let i = 0; i < 100000; i++) {
// 同时启动10w个定时器
setTimeout(fn, 0);
}
}
// 需要锁的情况
simulateAsyncOperation(incrementCounterWithLock);
锁会把 incrementCounterWithLock 中的逻辑都锁住,保证io时 即使切出,另外的并行处理由于拿不到锁,还是会切回来,从而保证 incrementCounterWithLock 中的所有逻辑都执行完,才会让出。这样就不会出现了上面的增加操作被覆盖的问题。
关键总结
对于单线程的nodejs,当操作同一对象时,锁io执行部分代码,不锁cpu执行部分代码。
延伸一下
上面演示的是单进程的本地锁 ,但是如果遇到多机多进程,则由于多进程之间无法访问相同的锁变量,锁无法生效,此时需要使用分布式锁。
分布式锁
示例, 以下是个简单示例,
const Redis = require('ioredis');
const Redlock = require('redlock');
// 创建一个 Redis 客户端
const redis = new Redis({
host: 'localhost',
port: 6379
});
// 创建一个 Redlock 对象
const redlock = new Redlock(
[redis],
{
driftFactor: 0.01, // 时间漂移因子
retryCount: 10, // 重试次数
retryDelay: 200, // 每次重试之间的时间(毫秒)
retryJitter: 200 // 重试抖动(毫秒)
}
);
// 分布式锁的使用
async function doSomething() {
const resource = 'locks:myResource';
const ttl = 1000; // 锁的有效期(毫秒)
try {
const lock = await redlock.lock(resource, ttl);
// 在这里执行你的代码
// ...
// 释放锁
await lock.unlock();
} catch (err) {
console.error('Failed to acquire lock', err);
}
}
doSomething();
分布式锁的关键在于锁需要在一个,多进程都可以的访问到的地方,常见的比如redis。
网友评论