前言
我时常有这样的疑问:
- std::mutex怎么就能保证后面的语句100%安全哪?
- CPU reordering就不会把这些语句重排到mutex前面执行?
- 而且各个CPU都是有L1、L2缓存的,如果mutex后面要访问的的变量在这些缓存中怎么办?
带着这些疑问我们先看看std::mutex是怎么实现的。
std::mutex::lock流程图
先给个图,再细说。
底层原理
先写个简单的cpp程序:
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::lock_guard, std::adopt_lock
std::mutex mtx; // mutex for critical section
int total=0;
void print_thread_id(int id) {
mtx.lock();
for(int i=0;i<10000;i++) total++;
mtx.unlock();
}
int main()
{
print_thread_id(1);
print_thread_id(2);
std::cout<<"total="<<total<<std::endl;
return 0;
}
我没起线程啊,只想看看std::mutex是怎么lock的, 这里调用print_thread_id两次也是故意的,因为第一次很多函数符号都没解决(函数@plt),遇到一个函数都要到ld里走一圈,很烦人。而第二次调用就用不着了。
在第二个print_thread_id上加断点,gdb中layout asm看汇编(也可以直接看glibc源码,实际源码有太多宏更不如汇编看的直接):
一路si(单步instruction执行),来到了关键指令:lock cmpxchg
$r8正好执行我们定义的全局变量mtx, 从上个截图可以看到mtx结构中第一个member是int类型的__lock(看到名字也能盲猜这是mutex::lock的关键),而edi=1。
cmpxchg指令语法如下(请把cmos_lock看成mtx.__lock, %edx为1):
cmpxchg会原子的完成下面的IF+赋值,不会让别的线程看到中间状态。
没被锁的情况下,destination(mtx.__lock)为0,accumulator($EAX)为0(pthread_mutex_lock+91 xor %eax,%eax使得eax的值为0),故ZF标志位被置为1,同时把source(1,pthread_mutex_lock+86 mov 1,%edi)存入mtx.__lock(表示锁上了)。
被别人锁的情况,请看下面的调试:
至于mtx.__lock会不会被各个CPU缓存,或者此指令后面的指令会不会被CPU重排,查了一些资料,也没弄100%明白,姑且先认为是吧。不然这么底层的东西早就造成大量问题了。
附上一些讨论:
concurrency - Is x86 CMPXCHG atomic, if so why does it need LOCK? - Stack Overflowhttps://stackoverflow.com/questions/27837731/is-x86-cmpxchg-atomic-if-so-why-does-it-need-lock
multithreading - Do locked instructions provide a barrier between weakly-ordered accesses? - Stack Overflowhttps://stackoverflow.com/questions/50280857/do-locked-instructions-provide-a-barrier-between-weakly-ordered-accesses
https://heather.cs.ucdavis.edu/matloff/public_html/50/PLN/lock.pdfhttps://heather.cs.ucdavis.edu/matloff/public_html/50/PLN/lock.pdf
如果此处发现mtx.__lock已经是1,也就是别人锁上了,则转系统调用202号(sys_futex)挂起当前线程。