排队买奶茶秒懂线程锁

文章摘要

本文用奶茶店排队买奶茶的比喻,生动解释了三种并发控制机制:1)原子加锁——直接购买(无人排队时);2)自旋——短暂等待(前面人少时);3)阻塞——排队叫号(人多时)。这三种方式分别对应高效抢占、短时等待和长时挂起的场景,正如pthread_mutex_lock采用的"先抢-再等-最后挂起"策略,在保证效率的同时合理利用系统资源。文章通过生活场景类比,清晰说明了不同锁机制的应用场景和优劣。


1. 原子加锁 —— 试试能不能直接买到

想象你去奶茶店,发现柜台前没人排队,你直接走上去,一把就拿到了点单的机会,马上下单买奶茶。这就像原子加锁

“柜台没人,我直接上!”


2. 自旋 —— 站在柜台旁边等一小会儿

如果你发现柜台前有一个人正在点单,但你觉得他很快就会点完,于是你就在柜台旁边等着,时不时探头看看他点完没有。
这就是自旋

“他快点完了吧?我再等一会儿,说不定马上轮到我了!”

你不会离开柜台,也不会去干别的事,就是原地等着,随时准备冲上去。


3. 阻塞 —— 排到长队里,等叫号

如果你发现前面的人点单很慢,或者前面排了好几个人,你觉得等下去太浪费时间了。于是你决定去旁边的休息区坐着,等服务员叫号
这就是阻塞

“算了,前面人太多,我先去休息区,等叫到我再过来。”

你把“排队”的权利交给了服务员(操作系统),等轮到你时,服务员会叫你回来点单。


4. 总结对比

  • 原子加锁:没人排队,直接买到奶茶,效率最高。
  • 自旋:前面就一个人,等一小会儿,随时准备冲上去,适合等待时间很短的情况。
  • 阻塞:人太多,去休息区等叫号,省力但响应慢一点,适合等待时间较长的情况。

5. 现实意义

  • 这样做的好处是:如果很快能买到奶茶,就不用浪费时间去休息区又回来;但如果要等很久,就不在柜台傻等,去休息区更省力
  • 这正是 pthread_mutex_lock 的三步走策略:先抢、再等、最后挂起,既高效又节省资源。

我们来用更技术化的比喻,并结合代码和系统层面的实现细节,帮助你深入理解 pthread_mutex_lock 的三步走策略。


一、技术化比喻

1. 原子加锁(CAS,Compare-And-Swap)

比喻:
就像你和一群同事抢占一台打印机。你们都用一条短信发给打印机:“如果你现在空闲(状态=0),请把状态改成‘我在用’(状态=1)。”
只有第一个发出请求并成功的人能用上打印机,其他人会发现状态已经变了。

技术点:

  • 这一步用的是原子操作(如 x86 的 lock cmpxchg 指令),保证多线程下不会出现竞态。
  • 代码层面通常是 atomic_compare_and_swap__sync_val_compare_and_swap

2. 自旋(Spin)

比喻:
没抢到打印机的同事,站在打印机旁边,不断地问:“你空了吗?你空了吗?……”
他们不会离开,也不会做别的事,就是一直盯着打印机,等它空出来。

技术点:

  • 线程在用户态死循环检查锁状态,期间会调用 cpu_relax()(如 x86 的 pause 指令)减少功耗和总线压力。
  • 代码层面是一个 for 循环,不断尝试原子加锁。

3. 阻塞(Block)

比喻:
如果打印机一直没空,等的人觉得太浪费时间了,于是登记一下,去休息室等着。等打印机空了,管理员(操作系统)会通知他们回来。

技术点:

  • 线程调用系统调用(如 Linux 的 futex),把自己挂起,进入内核态,等待被唤醒。
  • 这样 CPU 可以去做别的事情,不会被浪费。
  • 代码层面是 futex_waitfutex_wake

二、代码和系统层面实现

1. 原子加锁

if (__sync_bool_compare_and_swap(&mutex->__lock, 0, 1)) {
    // 获得锁,直接返回
    return 0;
}
  • 这里的 __sync_bool_compare_and_swap 是 GCC 的原子操作内建函数。

2. 自旋

for (int i = 0; i < SPIN_COUNT; ++i) {
    if (__sync_bool_compare_and_swap(&mutex->__lock, 0, 1)) {
        return 0;
    }
    cpu_relax(); // x86下是pause指令,ARM下是yield
}
  • SPIN_COUNT 是自旋的最大次数,通常几十到几百次。
  • cpu_relax() 防止CPU过热和总线压力。

3. 阻塞(futex)

while (true) {
    if (__sync_bool_compare_and_swap(&mutex->__lock, 0, 1)) {
        return 0;
    }
    // 进入内核,挂起线程
    futex(&mutex->__lock, FUTEX_WAIT, 1, NULL, NULL, 0);
}
  • futex 是 Linux 的系统调用,只有在竞争时才进入内核,平时都在用户态完成。
  • 当锁释放时,另一个线程会调用 futex(&mutex->__lock, FUTEX_WAKE, 1, NULL, NULL, 0); 唤醒等待的线程。

三、系统层面流程图

  1. 用户态:先用原子操作抢锁,抢到就走。
  2. 用户态:抢不到,短暂自旋(用户态循环)。
  3. 内核态:自旋还抢不到,调用 futex 进入内核,线程挂起,等待唤醒。

只有在锁竞争激烈时,才会频繁进入内核,平时都在用户态完成,效率很高。


四、总结

  • 原子加锁:用户态,最快,零开销。
  • 自旋:用户态,适合短等待,避免频繁进内核。
  • 阻塞:内核态,适合长等待,节省CPU资源。

这种三步走策略,兼顾了性能资源利用率,是现代高性能多线程库的标准做法。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值