【单线程就不用锁了吗? 你可能忽略了 nodejs中的锁】

单线程天然的优势

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

原因:

同时处理时, 可以看出 由于遇到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。

  • 30
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值