Yupureki:个人主页✨个人专栏:《C》 《算法》《Linux系统编程》《高并发内存池》《MySQL数据库》《个人在线OJ平台》Yupureki的简介:目录1. 线程互斥1.1 前置知识1.2 为什么需要线程互斥1.3 互斥锁1.4 常用接口1.4.1 初始化与销毁1.4.2 加锁与解锁1.4.3 互斥锁属性对象1.5 测试1.6 互斥锁的封装2. 线程同步2.1 条件变量2.2 常见接口2.2.1 初始化与销毁2.2.2 等待条件2.2.3 唤醒等待线程2.2.4 条件变量属性对象2.3 条件变量和互斥锁的使用顺序2.4 测试2.5 条件变量的封装3. 线程安全问题3.1 概念3.2 常见导致非线程安全/非重入的原因1. 线程互斥1.1 前置知识共享资源临界资源多线程执行流被保护的共享的资源就叫做临界资源临界区每个线程内部访问临界资源的代码就叫做临界区互斥任何时刻互斥保证有且只有一个执行流进入临界区访问临界资源通常对临界资源起保护作用原子性后面讨论如何实现不会被任何调度机制打断的操作该操作只有两态要么完成要么未完成1.2 为什么需要线程互斥线程之间共享进程的内存空间多个线程可以同时访问同一个变量或资源。如果不对访问进行控制可能出现以下情况线程 A 读取变量x 10线程 B 读取变量x 10线程 A 将x加 1写回x 11线程 B 将x加 1写回x 11原本希望x变成 12结果却变成了 11。这就是竞态条件。在这里变量x就是临界资源我们需要保护该资源使得每次访问该资源的时候最多只有一个线程。线程之间无法同时访问就是互斥的1.3 互斥锁Linux 中最常用的线程互斥工具是互斥锁mutexmutual exclusion。互斥锁有两种状态锁定locked某线程持有锁未锁定unlocked没有线程持有锁基本使用流程在访问共享资源前加锁pthread_mutex_lock访问共享资源临界区访问结束后解锁pthread_mutex_unlock如果某个线程尝试加锁但锁已被其他线程占用该线程会阻塞直到锁被释放。1.4 常用接口1.4.1 初始化与销毁接口描述pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr)动态初始化互斥锁可指定属性attr为NULL时使用默认属性。pthread_mutex_destroy(pthread_mutex_t *mutex)销毁互斥锁释放相关资源。销毁前锁必须处于未锁定状态。PTHREAD_MUTEX_INITIALIZER静态初始化宏用于编译时初始化具有默认属性的互斥锁。例如pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;1.4.2 加锁与解锁接口描述pthread_mutex_lock(pthread_mutex_t *mutex)加锁。如果锁已被其他线程持有调用线程会阻塞直到锁可用。pthread_mutex_trylock(pthread_mutex_t *mutex)尝试加锁。如果锁可用立即加锁并返回 0如果锁已被占用立即返回EBUSY不阻塞。pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abs_timeout)限时加锁。若在指定绝对时间前无法获得锁则返回ETIMEDOUT。pthread_mutex_unlock(pthread_mutex_t *mutex)解锁。由持有锁的线程调用释放锁。1.4.3 互斥锁属性对象接口描述pthread_mutexattr_init(pthread_mutexattr_t *attr)初始化互斥锁属性对象。pthread_mutexattr_destroy(pthread_mutexattr_t *attr)销毁互斥锁属性对象。pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type)设置互斥锁类型如PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE等。pthread_mutexattr_gettype(...)获取互斥锁类型。pthread_mutexattr_setpshared(...)设置互斥锁的共享范围进程内或进程间。1.5 测试不加锁#include pthread.h #include stdio.h pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; int shared_counter 0; void* increment(void* arg) { for (int i 0; i 100000; i) { //pthread_mutex_lock(mutex); // 加锁 shared_counter; // 临界区 //pthread_mutex_unlock(mutex); // 解锁 } return NULL; } int main() { pthread_t t1, t2; pthread_create(t1, NULL, increment, NULL); pthread_create(t2, NULL, increment, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf(Final counter: %d\n, shared_counter); // 200000 pthread_mutex_destroy(mutex); return 0; }我们期望的结果是200000但由于资源竞争的激烈结果只有101596。能看出加锁的重要性加锁:#include pthread.h #include stdio.h pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; int shared_counter 0; void* increment(void* arg) { for (int i 0; i 100000; i) { pthread_mutex_lock(mutex); // 加锁 shared_counter; // 临界区 pthread_mutex_unlock(mutex); // 解锁 } return NULL; } int main() { pthread_t t1, t2; pthread_create(t1, NULL, increment, NULL); pthread_create(t2, NULL, increment, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf(Final counter: %d\n, shared_counter); // 200000 pthread_mutex_destroy(mutex); return 0; }加锁后结果正常避免了资源竞争。但加锁的时候也破坏了线程的同步性其他的线程由于没拿到锁只能在临界区外干瞪眼什么也干不了。因此加锁会导致效率的下降1.6 互斥锁的封装#pragma once #include iostream #include pthread.h #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) class mylock{ public: mylock() { int n pthread_mutex_init(_lock,nullptr); if(n ! 0) { ERR_EXIT(mutex init); return; } //std::coutmutex init success!std::endl; } int lock() { return pthread_mutex_lock(_lock); } int unlock() { return pthread_mutex_unlock(_lock); } pthread_mutex_t* get_lock() { return _lock; } ~mylock() { pthread_mutex_destroy(_lock); } private: pthread_mutex_t _lock; };2. 线程同步2.1 条件变量没有拿到锁的线程只能在外面一直等待当锁归还的时候怎么办是不是也得抢?想象一下一堆线程竞争同一把锁是否会造成某些线程一直拿不到锁的情况?这就造成了效率低下的问题。不信?测试一下#include pthread.h #include stdio.h pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; int shared_counter 0; void* increment(void* arg) { for (int i 0; i 100000; i) { pthread_mutex_lock(mutex); // 加锁 printf(%s拿到了锁\n,(char*)arg); shared_counter; // 临界区 pthread_mutex_unlock(mutex); // 解锁 } return NULL; } int main() { pthread_t t1, t2,t3,t4; pthread_create(t1, NULL, increment, (void*)1); pthread_create(t2, NULL, increment, (void*)2); pthread_create(t3, NULL, increment, (void*)3); pthread_create(t4, NULL, increment, (void*)4); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL); printf(Final counter: %d\n, shared_counter); // 200000 pthread_mutex_destroy(mutex); return 0; }我们可以发现在某段时间内一直都是线程1拿到的锁刚还又给拿回来了相当于左手倒右手因此为了解决这个问题我们期望线程们拿锁应该是有顺序的即像一个队列一样先来先到。这就是条件变量同步在保证数据安全的前提下让线程能够按照某种特定的顺序访问临界资源从而有效避免饥饿问题叫做同步竞态条件因为时序问题而导致程序异常我们称之为竞态条件。在线程场景下这种问题也不难理解2.2 常见接口2.2.1 初始化与销毁接口描述pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr)动态初始化条件变量可指定属性attr为NULL时使用默认属性。pthread_cond_destroy(pthread_cond_t *cond)销毁条件变量释放相关资源。销毁前不应有线程在等待。PTHREAD_COND_INITIALIZER静态初始化宏用于编译时初始化具有默认属性的条件变量。例如pthread_cond_t cond PTHREAD_COND_INITIALIZER;2.2.2 等待条件接口描述pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)阻塞等待条件变量。调用前必须已锁定mutex。该函数会原子地释放mutex并阻塞线程直到被唤醒。被唤醒后函数返回前会重新获取mutex。pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)限时等待。若在指定的绝对时间前未被唤醒则返回ETIMEDOUT。用法与pthread_cond_wait类似但增加了超时机制。2.2.3 唤醒等待线程接口描述pthread_cond_signal(pthread_cond_t *cond)唤醒至少一个等待该条件变量的线程。如果没有线程在等待则无效果。pthread_cond_broadcast(pthread_cond_t *cond)唤醒所有等待该条件变量的线程。2.2.4 条件变量属性对象接口描述pthread_condattr_init(pthread_condattr_t *attr)初始化条件变量属性对象。pthread_condattr_destroy(pthread_condattr_t *attr)销毁条件变量属性对象。pthread_condattr_setpshared(...)设置条件变量的共享范围进程内或进程间。pthread_condattr_getpshared(...)获取条件变量的共享范围。pthread_condattr_setclock(...)设置pthread_cond_timedwait使用的时钟如CLOCK_MONOTONIC。2.3 条件变量和互斥锁的使用顺序条件变量与互斥锁的配合使用有明确的顺序要求这直接影响程序的正确性。通常等待线程必须先加锁再调用pthread_cond_wait唤醒线程则应先加锁、改变条件然后调用pthread_cond_signal最后解锁。这个顺序并非任意而是基于条件变量的工作机理和避免竞态的需要。等待线程的正确顺序pthread_mutex_lock(mutex); while (!condition) { pthread_cond_wait(cond, mutex); // 原子地释放锁并阻塞 } // 条件满足访问共享资源 pthread_mutex_unlock(mutex);为什么必须先加锁pthread_cond_wait要求调用前互斥锁已被锁定这是函数的硬性规定。更关键的是条件变量必须与互斥锁配合使用以保护条件的检查。如果先wait再lock则无法原子地检查条件和进入等待可能导致错过唤醒。为什么while而不是if因为存在虚假唤醒while循环可以重新检查条件确保只有条件真正成立时才继续执行。唤醒线程的正确顺序pthread_mutex_lock(mutex); // 修改共享条件如将 ready 置为 1 condition 1; pthread_cond_signal(cond); // 或 broadcast pthread_mutex_unlock(mutex);为什么先加锁再 signal最后解锁避免条件丢失如果在修改条件前就调用signal那么等待线程可能尚未进入wait状态从而错过唤醒。先加锁修改条件保证条件的改变和信号的发送是原子的等待线程要么在条件改变前进入等待随后被唤醒要么在条件改变后直接看到条件成立而不进入等待。防止竞态如果先signal再解锁在解锁之前等待线程可能已经醒来并尝试加锁但由于锁仍被持有等待线程会短暂阻塞但这是无害的反之如果先解锁再signal可能在解锁和signal之间插入另一个等待线程的加锁造成不必要的调度。是否可以解锁后再 signal从 POSIX 规范看pthread_cond_signal可以在不持有锁的情况下调用此时不会丢失唤醒因为等待线程在wait中会检查条件受锁保护。但为了代码简洁性和避免优先级反转等问题推荐在持有锁的情况下调用 signal。这样能确保条件变量与互斥锁的协同工作最可靠。两种常见误用及其后果误用后果等待线程先wait再加锁编译错误或未定义行为因为pthread_cond_wait要求锁已被锁定。唤醒线程先signal再修改条件等待线程可能在条件被修改前就收到信号导致条件仍不成立时被唤醒再次进入等待可能错过真正的唤醒即丢失唤醒。省流等待线程加锁 →pthread_cond_wait内部释放锁→ 醒来后重新获得锁 → 解锁。唤醒线程加锁 → 修改条件 →pthread_cond_signal→ 解锁。2.4 测试#include pthread.h #include unistd.h #include stdio.h pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond PTHREAD_COND_INITIALIZER; int shared_counter 0; bool done false; void* increment(void* arg) { while(!done) { pthread_mutex_lock(mutex); if(shared_counter 100) { done true; pthread_cond_broadcast(cond); pthread_mutex_unlock(mutex); break; } shared_counter; printf(%s拿到了锁当前计数器: %d\n,(char*)arg, shared_counter); pthread_mutex_unlock(mutex); usleep(100); // 稍微延迟让其他线程有机会执行 } printf(%s退出\n,(char*)arg); return NULL; } int main() { pthread_t t1, t2,t3,t4; pthread_create(t1, NULL, increment, (void*)1); pthread_create(t2, NULL, increment, (void*)2); pthread_create(t3, NULL, increment, (void*)3); pthread_create(t4, NULL, increment, (void*)4); while(shared_counter 100) { pthread_cond_signal(cond); } pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL); printf(Final counter: %d\n, shared_counter); pthread_mutex_destroy(mutex); pthread_cond_destroy(cond); return 0; }保证了线程按照顺序拿锁2.5 条件变量的封装#pragma once #include iostream #include pthread.h #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0) class mycond{ public: mycond() { int n pthread_cond_init(_cond,nullptr); if(n ! 0) { ERR_EXIT(cond init); return; } //std::coutcond init success!std::endl; } void signal() { pthread_cond_signal(_cond); } void wait(pthread_mutex_t* lock) { pthread_cond_wait(_cond,lock); } void broadcast() { pthread_cond_broadcast(_cond); } pthread_cond_t* get_cond() { return _cond; } ~mycond() { pthread_cond_destroy(_cond); } private: pthread_cond_t _cond; };3. 线程安全问题3.1 概念线程安全Thread Safety定义如果一个函数或数据结构在多线程环境中被多个线程同时调用仍然能正确工作即共享数据保持一致性不会出现竞态条件则称它是线程安全的。可重入性Reentrancy定义一个函数在执行过程中可以被中断比如被信号处理函数或另一个线程调用并且中断后再次进入该函数时仍然能正确运行不会破坏之前调用的状态则称它是可重入的。3.2 常见导致非线程安全/非重入的原因非线程安全使用未保护的全局或静态变量返回指向内部静态缓冲区的指针如asctime、ctime多个线程同时修改同一文件描述符未加锁非重入使用静态或全局变量来保存状态如strtok调用非可重入函数如malloc、printf在信号处理中通常不安全使用锁因为同一线程再次进入会死锁3.3 如何实现线程安全和可重入实现线程安全加锁保护临界区如pthread_mutex_lock/unlock原子操作对简单计数器用__sync_fetch_and_add或 C11 的atomic_*线程局部存储使用__thread或pthread_key_t实现可重入避免使用全局/静态数据将状态作为参数由调用者传入如strtok_r只使用局部变量存储在栈上不调用任何不可重入的函数不操作共享资源如文件、锁、信号量