美文网首页
【单线程就不用锁了吗? 你可能忽略了 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。

相关文章

  • 简圈冷笑话|没锁

    -今天你被锁了吗? -为什么要锁我?我压根就没写过文啊。 -今天你被锁了吗? -为什么要锁我?我长得很敏感?我只是...

  • Node进程

    Nodejs是单线程的,单线程好处是程序状态是单一的,没有多线程情况下没有锁、线程同步的问题,但是CPU是多核的,...

  • 锁就锁了

    莫名其妙,昨天一篇文章被锁定了,无所谓,本来就是写给自己看看的。朋友说多动笔预防老年痴呆。 昨天还剩一例新增病例,...

  • 锁锁锁你就知道锁

    简书的审核小姐姐…… 我不知道你是否真实存在…… 不过,我还是有话想对你说…… 这个当妹子的,还是软一点好…… 不...

  • Java并发锁之ReentrantLock和ReentrantR

    锁类型: 可重入锁:在执行对象中所有同步方法不用再次获得锁可中断锁:在等待获取锁过程中可中断公平锁: 按等待获取锁...

  • 又锁

    昨天说好的,今天谈“分离”的,结果刚发出来,简宝玉就秒锁了。 我得好好想想哪个词涉敏了。好吧,不用更文了。爱锁就锁...

  • 今天你被锁了吗?

    简叔在玩疯狂的(锁文游戏),友友你中招了吗?大家都来比一下。 锁了一篇就是倔强青铜 锁了两篇就是秩序白银 锁了三篇...

  • 并发里面的各种锁

    本文将讲解并发过程中可能会用到的各种锁,分别为重量级锁,自旋锁,自适应自旋锁,轻量级锁,偏向锁,悲观锁,乐观锁.....

  • NodeJs多线程、多进程、定时任务

    JS是单线程语言,减少了线程间切换的开销,且不用考虑锁的问题,因此适合IO密集型项目。JS的单线程,其实指的是js...

  • 又锁了,发了就锁

    以后就得说话越来越少才行,不然就会被锁

网友评论

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

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