英伟达C++ tegra面试:mutex底层原理是什么?

B站影视 内地电影 2025-08-15 00:04 2

摘要:今儿我们来聊聊计算机江湖里一个贼拉关键的玩意儿——互斥锁(江湖人称Mutex, Mutual Exclusion 的缩写,听着就挺排外的哈)。

今儿我们来聊聊计算机江湖里一个贼拉关键的玩意儿——互斥锁(江湖人称 Mutex, Mutual Exclusion 的缩写,听着就挺排外的哈)。

现在这世道,程序动不动就搞多线程、多进程,跟赶大集似的,一堆人(线程)乌泱泱都想抢同一个宝贝(共享资源),这不乱套了吗?

数据打架、结果跑偏那都是分分钟的事儿!

所以啊,就得请出咱们这位镇场子的狠角色——互斥锁,专治各种不服,保证同一时间,就一个狠人能摸到那宝贝疙瘩!

一、互斥锁是个啥路数?

简单粗暴!它就相当于给那“藏宝洞”(专业点叫临界区)门口杵了个彪形大汉。这“藏宝洞”里头放的啥?就是你程序里那些大家伙(线程/进程)都眼红的共享资源

想进?行啊~

规则就一条: 先拿钥匙(Lock)!钥匙就一把,谁抢到谁进!进去办事儿,办完立马把钥匙交出来(UnLock),下一个兄弟接着来!这不就和谐了?完美避免“数据打架”!

1、Lock(上锁):

线程大哥大摇大摆走到门口,伸手一摸——“哎哟,钥匙不在?被隔壁老王拿走了?” 行,那你就老实蹲墙角等着,别瞎嚷嚷,CPU 给你省着点用,去睡个觉先!

2、Unlock(放锁):

老王办完事儿,把钥匙“啪”一声扔门口:“下一个!” 睡觉的兄弟立马被叫醒:“轮到我了!” 抄起钥匙就往里冲!

就这么简单粗暴,保证同一时间,就一个线程在临界区蹦迪,其他兄弟?排队!别急!

1、硬件派:

玩的就是原子操作!

啥叫原子?就是“一气呵成,中间不许断”!

比如“测试并设置”(Test-and-Set)、“比较并交换”(Compare-and-Swap),CPU 硬件直接给你包圆儿,一气呵成,贼拉靠谱!多核处理器也得乖乖听话!

2、软件党:

比如那个传说中的 Dekker算法Peterson算法,纯靠代码逻辑“掰手腕”。

老铁,玩锁不是闹着玩的,搞不好就把自己“锁”进去了!

三大“作死”行为,你可得记住了:

1、死锁(Deadlock):

想象一下:线程A拿着锁1,死活等着锁2;线程B拿着锁2,死活等着锁1。好家伙,俩大爷互相卡脖子,谁也不松手!结果就是——全卡死!

破解之法: 要么大家按统一顺序拿锁(比如都先1后2),要么用try_lock,拿不到就溜,别硬刚!这叫“能屈能伸,方为线程好汉”!

2、优先级反转(Priority Inversion):

高优先级线程急着办事,结果发现钥匙被低优先级线程捏着,慢悠悠地磨蹭。高优先级大哥只能干瞪眼?这不乱套了!

破解之法: 上优先级继承!让拿钥匙的低优先级线程临时“升职加薪”,变成高优先级,赶紧把事儿办了,把钥匙交出来!这叫“为了大局,临时抱佛脚”!

3、锁的粒度——“锁得太大” or “锁得太碎”?

锁太大:整个函数甚至模块都锁了,那还玩啥并发?并发变串行,性能直接躺平!

锁太碎:到处都是小锁,管理开销巨大,代码乱成一锅粥,维护起来想哭!

这俩兄弟,看着像,脾气差远了!

自旋锁(Spinlock):

这哥们儿轴!拿不到锁?不睡觉!不放弃! 就在门口“busy-waiting”,CPU疯狂空转:“好了没?好了没?”——CPU 烤机专用锁! 只适合临界区贼短的情况,比如就几条指令,转几圈就完了。时间长了?CPU 直接给你干冒烟!

互斥锁(Mutex):

这才是“文明人”!拿不到锁?麻利儿地睡觉去! 把CPU让给其他兄弟干活。等拿锁的哥们儿一放钥匙,立马有人喊:“醒醒!轮到你了!” 瞬间满血复活!省电、高效、适合临界区较长的场景!std::mutex 的 lock 就是这路数——拿不到?睡!unlock 一放锁?喊人!

在C++里,光有 std::mutex 还不够,还得配“锁管理器”:

std::lock_guard

一键锁死,死后解锁”!构造时自动 lock,析构时(比如出了作用域)自动 unlock。简单粗暴,性能贼好,适合“进去-干活-出来”一条龙,不搞花里胡哨的。

std::unique_lock

锁界变形金刚”!更灵活!能手动 lock/unlock,能延迟加锁,还能转移锁的“所有权”!

关键一点:想用条件变量(condition_variable)?必须用它!std::lock_guard 太“死板”,搞不定!

你以为 std::mutex是啥高科技?错!它就是个“包工头”!真正的“苦力活”它可不干!咱瞅瞅它的源码:class mutex : private __mutex_base // 基类:__mutex_base{ void lock { int __e = __gthread_mutex_lock(&_M_mutex); // 喊人!干活去! // ... } void unlock { __gthread_mutex_unlock(&_M_mutex); // 喊人!收工! } // ...}

看到没?lock 和 unlock 里面干了啥?

直接调用 __gthread_mutex_lock__gthread_mutex_unlock

这俩货又是谁?

class __mutex_base // mutexd的基类{ protected: typedef __gthread_mutex_t __native_type; __native_type _M_mutex = __GTHREAD_MUTEX_INIT; // 初始化宏 constexpr __mutex_base noexcept = default; // ... }typedef pthread_mutex_t __gthread_mutex_t; // _M_mutex的原始类型就是pthread_mutex_t#define __GTHREAD_MUTEX_INIT PTHREAD_MUTEX_INITIALIZER // 初始化宏是glibc库的初始化宏

所以,std::mutex 本质上就是对 glibc 里的 pthread_mutex_t 的一层马甲!真正的“脏活累活”,全交给 pthread 库了!

C++标准库:我负责优雅,你们负责拼命!

typedef union { struct __pthread_mutex_s { int __lock; // !

核心中的核心:__lock 这个int值,就是锁的“状态指示灯”:

0:绿灯! 没人用,空着呢!冲啊兄弟!1:黄灯! 有人用了,但没竞争!别急,等会儿。2:红灯! 有人用了,而且后面排大队了!竞争激烈!

注意! 这int读写不是原子的!你不能直接 if (__lock == 0) __lock = 1;,这中间可能被其他线程插一脚!所以,必须用原子操作!比如 test-and-set 或者 compare-and-swap (CAS)!这叫“原子操作,安全第一”!

pthread_mutex 的牛X之处,在于它用了 futex!全称 Fast Userspace muTEX,翻译过来就是“快速用户态互斥锁”。

这玩意儿的精髓就是:能不进内核,就绝不进内核! 节省开销!

#define __lll_lock(futex, private) \ ((void) ({ \ int *__futex = (futex); \ if (atomic_compare_and_exchange_bool_acq (__futex, 1, 0)) { \ // CAS失败!说明__lock不是0!得进内核了! __lll_lock_wait_private (__futex); \ } \ }))

这代码啥意思?翻译成人话:

“试探性进攻”:我先瞅一眼 __lock 是不是 0?

“原子抢钥匙”:如果是 0,我立马用 CAS 操作,把它从 0 改成 1!这一套操作在用户态瞬间完成!没有系统调用!没有上下文切换!贼快!

“抢不到就喊爹”:如果 __lock 不是 0(说明是 1 或 2),CAS 失败了!那没办法了,只能调用 __lll_lock_wait_private,正式进入内核,请求 futex 系统调用帮忙!

这叫啥?这叫“乐观锁”!先假设没竞争,抢到了就赢麻了;抢不到,再老老实实排队!

#define __lll_unlock(futex, private) \ ((void) ({ \ int *__futex = (futex); \ int __oldval = atomic_exchange_rel (__futex, 0); // 原子地把__lock设为0,并拿到旧值 if (__oldval > 1) { // 旧值大于1?说明曾经是2!有兄弟在等! __lll_lock_wake_private (__futex); // 喊内核!叫醒一个等锁的兄弟! } \ }))

解锁流程:

“归还钥匙”:用 atomic_exchange_rel 原子操作,把 __lock 直接设为 0。这个操作会返回设置前的旧值

“看情况喊人”:

如果旧值是 1:说明只有我一个线程在用,没竞争。直接走人! 不用叫任何人!如果旧值是 2:说明曾经有竞争!有兄弟在内核里“睡大觉”呢!赶紧调用 __lll_lock_wake_private,进入内核,通过 futex_wake 系统调用,把等锁的兄弟喊醒一个!

“报到”:线程B调用 futex_wait(&__lock, expected_val),说:“大佬,我等 __lock 变成 expected_val(通常是0)”。

“再确认”:内核拿到 &__lock 的地址,先计算哈希,找到对应的队列,上自旋锁,然后再次检查 *uaddr 的值! 为啥?防止在你进内核的路上,锁已经被释放了!这就是“双重检查”!

“请入席”:如果检查发现锁还是没释放(*uaddr != expected_val),内核就把线程B的状态设为 TASK_INTERRUPTIBLE(可中断睡眠),把它插入到等待队列里,然后释放自旋锁,并调用调度器让出CPU。线程B开始“睡觉”。

“点名”:线程A调用 futex_wake(&__lock, nr_wake=1),说:“大佬,把等 __lock 的兄弟叫醒一个!”

“找人”:内核同样计算哈希,找到队列,上自旋锁,遍历队列,找到一个正在等这个 futex 的线程(比如B)。

“叫醒”:把线程B从等待队列移除,加入到CPU的就绪队列,等待被调度。内核返回。

灵魂拷问:unlock时,state=0和futex_wake之间,如果C抢锁了,B醒来怎么办?

完美设计!不怕!

A解锁:__lock 从 2 -> 0,调用 futex_wake 喊B。

就在A调用futex_wake和B被真正唤醒调度的间隙,线程C来了!它执行 lock:

看 __lock 是 0!CAS操作,把 __lock 从 0 -> 1!成功抢到锁!

B被唤醒,调度到CPU上执行:

B从 futex_wait 返回,回到 pthread_mutex_lock 的代码里。关键来了! B不会直接认为“我拿到锁了”!它会重新进入__lll_lock的流程!此时B会再次执行 atomic_compare_and_exchange_bool_acq (&__lock, 1, 0)!但这时 __lock 是 1(被C占了),CAS失败!于是B发现:“哦,还是没抢到”,它会再次进入 __lll_lock_wait_private,重新调用 futex_wait,继续睡觉

整个过程就像一场接力赛:

第一棒(用户态CAS): 能抢就抢,抢到就赢!第二棒(内核futex_wait): 抢不到?进内核“候车室”(等待队列)睡觉!第三棒(内核futex_wake): 有人放锁?内核“调度员”喊醒一个“候车”的!第四棒(醒来重试): 被喊醒的线程,还得重新跑第一棒(CAS)!抢到算你狠,抢不到继续睡!

所以,std::mutex 的高效,就在于它最大限度地减少了昂贵的系统调用,把“无竞争”的场景完美地留在了用户态! 这就是现代操作系统和库设计的智慧结晶!

老铁们,这把锁的奥义学废了没!下回想看哪些知识?评论区喊出来!点赞关注走起,咱下期接着唠!

来源:音视频开发老舅

相关推荐