
💡Yupureki:个人主页
✨个人专栏:《C++》 《算法》《Linux系统编程》《高并发内存池》《MySQL数据库》
🌸Yupureki🌸的简介:

目录
[1. 线程互斥](#1. 线程互斥)
[1.1 前置知识](#1.1 前置知识)
[1.2 为什么需要线程互斥?](#1.2 为什么需要线程互斥?)
[1.3 互斥锁](#1.3 互斥锁)
[1.4 常用接口](#1.4 常用接口)
[1.4.1 初始化与销毁](#1.4.1 初始化与销毁)
[1.4.2 加锁与解锁](#1.4.2 加锁与解锁)
[1.4.3 互斥锁属性对象](#1.4.3 互斥锁属性对象)
[1.5 测试](#1.5 测试)
[1.6 互斥锁的封装](#1.6 互斥锁的封装)
[2. 线程同步](#2. 线程同步)
[2.1 条件变量](#2.1 条件变量)
[2.2 常见接口](#2.2 常见接口)
[2.2.1 初始化与销毁](#2.2.1 初始化与销毁)
[2.2.2 等待条件](#2.2.2 等待条件)
[2.2.3 唤醒等待线程](#2.2.3 唤醒等待线程)
[2.2.4 条件变量属性对象](#2.2.4 条件变量属性对象)
[2.3 条件变量和互斥锁的使用顺序](#2.3 条件变量和互斥锁的使用顺序)
[2.4 测试](#2.4 测试)
[2.5 条件变量的封装](#2.5 条件变量的封装)
[3. 线程安全问题](#3. 线程安全问题)
[3.1 概念](#3.1 概念)
[3.2 常见导致非线程安全/非重入的原因](#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 中最常用的线程互斥工具是互斥锁(mutex,mutual 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 测试
不加锁
cpp
#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。能看出加锁的重要性
加锁:
cpp
#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 互斥锁的封装
cpp
#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::cout<<"mutex 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 条件变量
没有拿到锁的线程只能在外面一直等待,当锁归还的时候怎么办,是不是也得抢?
想象一下,一堆线程竞争同一把锁,是否会造成某些线程一直拿不到锁的情况?这就造成了效率低下的问题。
不信?测试一下
cpp
#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,最后解锁。 这个顺序并非任意,而是基于条件变量的工作机理和避免竞态的需要。
等待线程的正确顺序
cpppthread_mutex_lock(&mutex); while (!condition) { pthread_cond_wait(&cond, &mutex); // 原子地释放锁并阻塞 } // 条件满足,访问共享资源 pthread_mutex_unlock(&mutex);
为什么必须先加锁?
-
pthread_cond_wait要求调用前互斥锁已被锁定,这是函数的硬性规定。 -
更关键的是,条件变量必须与互斥锁配合使用,以保护条件的检查。如果先
wait再lock,则无法原子地检查条件和进入等待,可能导致错过唤醒。
为什么 while 而不是 if?
- 因为存在虚假唤醒,
while循环可以重新检查条件,确保只有条件真正成立时才继续执行。
唤醒线程的正确顺序
cpppthread_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 测试
cpp
#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 条件变量的封装
cpp
#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::cout<<"cond 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) -
只使用局部变量(存储在栈上)
-
不调用任何不可重入的函数
-
不操作共享资源(如文件、锁、信号量)