
线程栈主线程进程 main 栈特性1来源fork 复制父进程栈2可动态自动扩容3缺页容错特殊允许访问未映射页、不一定直接段错误的栈线程栈mem mmap(NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);标志MAP_STACK专门标记这块内存用作线程栈默认固定 8MB 大小一般够用不支持动态扩容空间用完直接栈溢出崩溃属于进程虚拟地址里一块独立匿名映射区域pthread_create → allocate_stackmmap 分配栈→ 调用sys_cloneclone封装了sys_clone)系统调用 clone 时把 mmap 得到的栈顶栈向下增长newsp传给内核内核新建 task_struct (LWP) 绑定该栈。int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...);线程之间虽说是“私有”的线程栈但线程之间也可以访问子线程的a变量虽然定义在其“私有”的线程栈上但因为线程之间的虚拟地址空间是共享的所以其它线程当然也可以通过地址访问到a变量。即只要拿到栈内变量地址线程语法上能访问但不推荐跨线程读写对方栈子线程退出时 mmap 栈会被 munmap 释放指针变成野指针属于 C/C 未定义行为不同系统 / 内核表现不一致有时正常、有时段错误。C封装线程先再认识2个接口设置线程名// 参数1 线程tid参数2 字符串名字 int pthread_setname_np(pthread_t thread, const char *name);获取线程名char buf[16]; pthread_getname_np(pthread_t tid, buf, sizeof(buf));pthread.hpp:#pragma once #include iostream #include pthread.h #include unistd.h #include functional #include string #include errno.h #include cstring #includecstdint using namespace std; namespace ThreadModlue { static uint32_t number 1;//bug多个线程同时构造时number存在数据竞争会出现重名线程 //定义在 cstdint不用int是两点1. 线程编号只能是正数 //2. uint32_t固定 32 位跨平台不会溢出 using func_t functionvoid(); class Thread { private: void EnableRunning() { cout_isrunningtrueendl; _isrunning true; } void EnableDetach() { cout _isdetachtrue endl; _isdetach true; } static void *Routine(void *args) { Thread *self static_castThread *(args); self-EnableRunning(); self-Detach(); pthread_setname_np(self-_tid, self-_name.c_str()); self-_func(); // 回调处理 return nullptr; } //C 普通成员函数隐藏第一个参数 this函数真实原型,所以Routine真实原型 //void Routine(Thread* this, void* args);与pthread_create 对线程入口函数的强制要求 //void* (*start_routine)(void*);只接收单个 void * 参数不能多参数。有冲突所以加static //static成员函数没有隐含this指针 public: Thread(func_t func) : _isdetach(false), _isrunning(false), _tid(0), _func(func) { _name thread- std::to_string(number); } void Detach() { if (_isrunning _isdetach) { cout调用了phread_detach,线程分离了endl; pthread_detach(_tid); } else if (_isdetach false) EnableDetach(); } bool Start() { if (_isrunning) return false; int n pthread_create(_tid, nullptr, Routine, this); if (n ! 0) { std::cerr create thread error: strerror(n) std::endl; return false; } else { std::cout _name create success std::endl; return true; } } bool Stop() { if (_isrunning) { int n pthread_cancel(_tid); if (n ! 0) { std::cerr stop thread error: strerror(n) std::endl; return false; } else { _isrunning false; std::cout _name stop std::endl; return true; } } return false; } bool Join() { if (_isdetach) { return false; } int n pthread_join(_tid, nullptr); if (n ! 0) { std::cerr join thread error: strerror(n) std::endl; return false; } else { std::cout _name join std::endl; return true; } } ~Thread() { } private: pthread_t _tid; bool _isrunning; bool _isdetach; func_t _func; string _name; }; };main.cc:#include pthread.hpp #include unistd.h #include vector using namespace ThreadModlue; void func() { while (1) { char name[128]; pthread_getname_np(pthread_self(), name, sizeof(name)); std::cout 我是一个新线程: name std::endl; // 我的线程的名字是什么呀debug sleep(1); } } int main() { Thread t(func); t.Detach(); t.Start(); sleep(5); t.Stop(); sleep(5); t.Join(); return 0; }运行结果线程局部存储TLS#include iostream #include pthread.h #include unistd.h #include cstring #include stdio.h using namespace std; static int count 0; string Addr(int c) { char addr[64]; snprintf(addr, sizeof(addr), %p, c); return addr; } void *routine1(void *args) { (void)args; while (1) { cout thread-1,count count [我来修改count], count: Addr(count) endl; count; sleep(1); } } void *routine2(void *args) { (void)args; while (1) { cout thread-2,count count count: Addr(count) endl; count; sleep(1); } } int main() { pthread_t tid1, tid2; pthread_create(tid1, nullptr, routine1, nullptr); pthread_create(tid2, nullptr, routine2, nullptr); pthread_join(tid1, nullptr); pthread_join(tid2, nullptr); return 0; }运行结果我们可以从运行结果看出count的地址并没有发生变化再换成__thread int count0后发现两个线程的count不一样了互不影响线程局部存储原理操作系统为每个线程单独分配 TLS 内存段线程访问时自动切换自身副本地址各不相同互不干扰完全隔离。线程互斥进程线程间的互斥相关背景概念• 共享资源• 临界资源多线程执行流被保护的共享的资源就叫做临界资源• 临界区每个线程内部访问临界资源的代码就叫做临界区• 互斥任何时刻互斥保证有且只有⼀个执行流进入临界区访问临界资源通常对临界资源起 保护作用• 原子性不会被任何调度机制打断的操作该操作只有两态要么完成 要么未完成下面我们看一段模拟抢票过程的代码#include stdio.h #include stdlib.h #include string.h #include unistd.h #include pthread.h int ticket 100; void *route(void *arg) { char *id (char *)arg; while (1) { if (ticket 0) { usleep(1000); printf(%s sells ticket:%d\n, id, ticket); ticket--; } else { break; } } return nullptr; } int main() { pthread_t t1, t2, t3, t4; pthread_create(t1, NULL, route, (void *)thread 1); pthread_create(t2, NULL, route, (void *)thread 2); pthread_create(t3, NULL, route, (void *)thread 3); pthread_create(t4, NULL, route, (void *)thread 4); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL); }运行结果截取一部分为什么会有数据不一致问题核心原因ticket--不是原子操作3条汇编0xFF00 mov ebx, ticket // 1. 从内存把ticket载入CPU寄存器ebx 0xFF02 sub ebx, 1 // 2. CPU运算寄存器内数值-1 0xFF04 mov ticket, ebx // 3. 把计算结果写回内存ticket这三步是分开执行的线程切换可以发生在任意两步中间所以数据就会错乱。eg内存开始ticket100线程 A 执行到0xFF04时发生线程切换此时保留线程的硬件上下文pc指针为0XFF04ebx为99切换到线程 B假如完整并且多次执行了三次汇编将ticket修改为 1切回线程 A从 PC0xFF04 继续执行直接把 ebx99 写回内存最终内存ticket99线程 B 的修改直接丢失数据完全错乱为什么会出现负数的情况当几个线程的ticket都为1时线程 Aif(ticket0)判断通过进入usleep主动放弃 CPU线程 B同样if(ticket0)判断通过进入usleep两个线程同时唤醒先后执行打印 ticket1 → ticket-- 最终内存 ticket -1出现超卖负数。usleep的作用人为拉长 “判断票数” 和 “修改票数” 之间的间隔因为进入usleep后当前线程休眠了cpu就有就会去调度其它线程了强制触发线程切换容易看到bug线程切换触发时机线程切走时机就是陷入内核时机时间片耗尽阻塞式系统调用sleep/IO 读写等等中断线程切回时机选择新的线程系统调用完成从内核态切回用户态时操作系统检查是否需要换线程pthread_mutex_t 互斥锁依赖头文件#include pthread.h两种初始化方法静态初始化全局锁---无需手动销毁程序结束自动释放pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;动态初始化局部锁 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);attr传NULL使用默认锁属性使用完毕必须调用pthread_mutex_destroy(mutex)释放资源。销毁接口int pthread_mutex_destroy(pthread_mutex_t *mutex);核心操作函数全原子执行函数作用阻塞特性pthread_mutex_lock(pthread_mutex_t *mutex)申请锁阻塞版锁被占用时线程挂起等待pthread_mutex_trylock(pthread_mutex_t *mutex)尝试申请锁非阻塞版拿不到锁直接返回错误不挂起线程.pthread_mutex_unlock(pthread_mutex_t *mutex)释放锁无阻塞归还锁资源加锁尽量加锁的范围粒度要比较细尽可能的不要包含太多的非临界区代码方式1全局锁int ticket 100; pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; // 方法1静态全局初始化 void *route(void *arg) { char *id (char *)arg; while (1) { pthread_mutex_lock(mutex); if (ticket 0) { usleep(1000); printf(%s sells ticket:%d\n, id, ticket); ticket--; pthread_mutex_unlock(mutex); } else { pthread_mutex_unlock(mutex); break; } } return nullptr; }方式2局部锁// 全局共享票数 int ticket 100; void *route(void *arg) { char *id (char*)arg; // 1. 定义局部互斥锁栈上局部变量 pthread_mutex_t mutex; // 2. 动态初始化锁attr传NULL使用默认属性 pthread_mutex_init(mutex, NULL); while (1) { // 加锁进入临界区 pthread_mutex_lock(mutex); if (ticket 0) { usleep(1000); printf(%s sells ticket:%d\n, id, ticket); ticket--; // 分支内解锁 pthread_mutex_unlock(mutex); } else { // 无票也要先解锁再退出循环 pthread_mutex_unlock(mutex); break; } } // 3. 销毁局部锁释放内核资源 pthread_mutex_destroy(mutex); return NULL; }2种方式的运行结果截取一部分发现确实这一次的结果正确了但是为什么绝大部分都是线程2在抢票呢甚至线程3和4根本就没有机会抢票我们不妨分析一下整个过程thread 2 拿到锁锁内 sleep 1ms 模拟卖票打印、ticket--、执行unlock释放锁代码无任何延时直接跳回while(1)顶部立刻执行lock此时 thread1、thread4和thread3还在内核就绪队列排队CPU 优先调度刚跑完的 thread2再次抢占锁循环往复就出现同一个线程连续打印十几张票的现象。怎么解决呢循环末尾加 usleep下一次循环抢锁前先延时一会让出 CPU 给其他线程void *route(void *arg) { char *id (char *)arg; while (1) { pthread_mutex_lock(mutex); if (ticket 0) { usleep(1000); printf(%s sells ticket:%d\n, id, ticket); ticket--; pthread_mutex_unlock(mutex); } else { pthread_mutex_unlock(mutex); break; } // 新增解锁后休眠给其他线程调度时间 usleep(1000); } return nullptr; }这次的运行结果c锁c自己也有锁#includemutex int ticket 100; mutex cpp_lock; void *route(void *arg) { char *id (char *)arg; while (1) { cpp_lock.lock(); if (ticket 0) { usleep(1000); printf(%s sells ticket:%d\n, id, ticket); ticket--; cpp_lock.unlock(); } else { cpp_lock.unlock(); break; } } return nullptr; }基于RAII风格的互斥锁test.cc:#include stdio.h #include stdlib.h #include string.h #include unistd.h #include pthread.h #include Mutex.hpp using namespace std; using namespace MutexModule; int ticket 100; class ThreadData { public: ThreadData(const string name, Mutex lock) : _name(name), _lockp(lock) { } ~ThreadData() { } string _name; Mutex _lockp;//为什么是引用 }; //函数形参写 只是接收外部对象时不拷贝 //类内部成员没写 初始化列表会执行拷贝生成独立副本 //想要全程共用同一把锁构造参数 和 类成员 必须同时是引用 / 指针只改一边没用。 void *route(void *arg) { ThreadData *td static_castThreadData *(arg); while (1) { { LockGuard guard(td-_lockp); if (ticket 0) { usleep(1000); printf(%s sells ticket:%d\n, td-_name.c_str(), ticket); ticket--; } else { break; } } usleep(1000); } return nullptr; } int main() { Mutex lock; pthread_t t1, t2, t3, t4; ThreadData *td1 new ThreadData(thread 1, lock); pthread_create(t1, NULL, route, td1); ThreadData *td2 new ThreadData(thread 2, lock); pthread_create(t2, NULL, route, td2); ThreadData *td3 new ThreadData(thread 3, lock); pthread_create(t3, NULL, route, td3); ThreadData *td4 new ThreadData(thread 4, lock); pthread_create(t4, NULL, route, td4); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL); delete td1,td2,td3,td4; return 0; }Mutex.hpp:#pragma once #include iostream #include pthread.h namespace MutexModule { //第一层 Mutex 类封装底层 C 锁 API class Mutex { public: Mutex() { pthread_mutex_init(_mutex, nullptr); } void Lock() { int n pthread_mutex_lock(_mutex); (void)n; } void Unlock() { int n pthread_mutex_unlock(_mutex); (void)n; } ~Mutex() { pthread_mutex_destroy(_mutex); } private: pthread_mutex_t _mutex; }; class LockGuard { public: LockGuard(Mutex mutex):_mutex(mutex)//这里但是这只作用在函数内部临时变量 mutex不影响类成员_mutex //比如mutex,等等确实是引用,会影响外面传入的锁但是_mutexmutex)如果参数没有引用就会发生值拷贝 { _mutex.Lock(); } ~LockGuard() { _mutex.Unlock(); } private: Mutex _mutex;//为什么是引用 }; }3层封装关系Mutex最底层基础封装直接封装 C 原生pthread_mutex_t负责锁的初始化、加锁、解锁、销毁LockGuard中层RAII 自动管理层构造函数创建 Guard 瞬间调用_mutex.Lock()拿锁析构函数Guard 离开作用域自动_mutex.Unlock()释放锁 作用消除手动 unlock杜绝漏解锁死锁实现 RAII 自动资源管理ThreadData最上层封装线程名字和锁特别说明内层{}内层{}作用LockGuard是局部栈对象离开包裹它的{}代码块时自动调用析构函数解锁。 如果不加这对大括号guard的生命周期会覆盖整个while(1)循环体锁会持有整整一轮循环。这样的话后面的usleep(1000)才能发挥无锁休眠其他线程可以抢锁的效果如果没有内层{}到了usleep(1000)仍然是持有锁其它线程不能抢占锁导致一个线程长期霸占锁。没有内层{} 有几个结论1.锁本身也是临界资源多线程同时竞争lock时申请锁的底层操作必须是 CPU 原子指令避免锁自身发生竞争错乱2.互斥锁本质能力并行转串行临界区代码同一时刻只允许一个线程执行消除数据竞争变相原子性: 临界区代码在执行期间不会被其它线程打断3.对临界资源的保护本质就是用锁对临界区代码的保护核心问题临界区内部加了锁后允许线程切换吗切换后会发生什么先说结论操作系统完全允许在线程持有锁时切换线程锁不会阻止内核的调度切换只会阻塞其他线程进入临界区切换后的行为当前线程拿着锁被切走、让出 CPU其他线程尝试执行pthread_mutex_lock时发现锁已被占用不能进入临界区等该线程重新被调度回来后继续执行剩余临界区代码直到主动unlock释放锁其他线程才能竞争后抢到锁再进入。临界区对外具备原子性对没有持有锁的外部线程来说只关心持有锁的线程要么完整走完临界区并释放锁要么完全不进入不关心临界区的中间执行状态理解锁互斥锁两种底层实现方式硬件级方案关闭时钟中断单 CPU 老式实现思路多cpu性能损耗极大不再使用软件级方案现代 CPU 通用原子交换指令xchgCPU 提供swap/xchg交换指令单条硬件指令、不可分割天然原子伪代码lock和unlock锁状态约定mutex1锁空闲可抢占mutex0锁已被其他线程持有线程 A、B 同时执行xchgb %al, mutex场景演示初始内存mutex1寄存器%al都会置为0线程 A 交换al1、内存mutex0→ 判断al0成功拿锁线程 B 随后交换al0、内存mutex0→ 判断失败循环重试等待关键结论交换是数据互换不是拷贝全局内存只有一份 mutex 变量谁交换到1谁就占有锁资源。