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

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

作者: wn777 | 来源:发表于2024-06-16 00:16 被阅读0次

    单线程天然的优势

    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。

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/fjnzqjtx.html