Linux 线程安全与线程同步
Linux 线程安全指多个线程并发执行同一段代码时不会出现不同结果。重入指同一函数被不同执行流再次调用。常见线程不安全情况包括不保护共享变量、返回静态指针等。死锁由互斥、请求保持、不剥夺、循环等待四个必要条件导致,破坏任一条件即可避免。线程同步用于解决线程饥饿问题,通过条件变量(如 pthread_cond_wait)实现线程间的等待与通知机制,确保临界资源按序访问。

Linux 线程安全指多个线程并发执行同一段代码时不会出现不同结果。重入指同一函数被不同执行流再次调用。常见线程不安全情况包括不保护共享变量、返回静态指针等。死锁由互斥、请求保持、不剥夺、循环等待四个必要条件导致,破坏任一条件即可避免。线程同步用于解决线程饥饿问题,通过条件变量(如 pthread_cond_wait)实现线程间的等待与通知机制,确保临界资源按序访问。

我们这里通过理解重入与线程安全的关系来理解线程安全。
线程安全即多个线程并发同一段代码时,不会出现不同的结果。
重入即同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入。一个函数在重入的情况下运行结果不会出现任何问题,这样的函数称为可重入函数,否则,就是不可重入函数。
死锁是指在一组进程或线程中的各个进程或线程均占有不会释放的资源,但因互相申请被其他进程或线程所占用的不会释放的资源而处于的一种永久等待的状态。
死锁都是人为产生的,我们可以规避掉的。
在纯互斥的场景下,由于我们的锁只有少量个,多个线程同时竞争锁,但是得到锁的只有一小部分线程,剩下的线程就会因为等待,产生'线程饥饿'问题。线程饥饿本质上就是抢夺不到锁的线程,即抢夺不到资源的线程在等待锁的释放。为了避免这里的饥饿的问题,我们就通过线程同步来在保证数据安全的前提下,让线程按照顺序访问临界资源。
当一个线程互斥的访问某个变量时,它可能在其他线程改变状态之前什么也做不了,比如一个线程访问队列时,发现队列为空,那么它只能等待,直到其他进程将一个节点添加到队列当中,这个时候我们就可以利用条件变量来规避这种情况。
#include <pthread.h>
int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_condattr_t* restrict attr);
返回值:成功返回 0,失败返回非 0 错误码
cond:指向要初始化的条件变量的指针,pthread_cond_t 是一个表示条件变量的数据类型
attr:指向条件变量属性对象的指针,传入 NULL 表示使用默认属性
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t* cond);
返回值:成功返回 0,失败返回非 0 错误码
cond:指向要销毁的条件变量的指针
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
返回值:成功返回 0,失败返回非 0 错误码
cond:指向要操作的条件变量的指针,条件变量用于线程之间的等待和通知机制
mutex:指向互斥锁的指针,互斥锁用于保护共享资源,确保线程安全
调用该函数时,线程会自动释放互斥锁 mutex,以便其他线程可以获取锁,当收到信号被唤醒后,线程会重新尝试获取互斥锁。
#include <pthread.h>
// 唤醒一个等待线程
int pthread_cond_signal(pthread_cond_t* cond);
// 唤起所有等待线程
int pthread_cond_broadcast(pthread_cond_t* cond);
返回值:成功返回 0,失败返回非 0 错误码
cond:指向要操作的条件变量的指针,条件变量是一种用于线程同步的机制,允许线程在某个条件不满足时阻塞,直到其他线程通知该条件已经满足。
如果一个线程执行 pthread_cond_broadcast,它会将所有等待该条件变量的线程全部唤醒,若执行 pthread_cond_signal,则只会唤醒至少一个等待该条件变量的线程,而非只唤醒当前线程。
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
using namespace std;
#define NUM 4
int cnt = 0;
// 条件变量函数的用法几乎与锁函数的用法完全等同
// 定义全局锁和全局条件变量
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* Count(void* args) {
pthread_detach(pthread_self()); // 线程分离,跑完就不管了,不在乎它的返回值
// Linux 是 64 位机,指针是 8 字节,uint 是 unsigned long long int
uint64_t num = (uint64_t)args;
cout << "Thread " << num << " is created success" << endl;
usleep(100000);
while (true) {
pthread_mutex_lock(&lock); // 这里 pthread_cond_wait 要在临界区的原因是:
// 因为 pthread_cond_wait 是让线程去等待,等待的原因一定是临界资源不就绪
// 而临界资源是否就绪,是通过判断得来的,判断也是访问临界资源,所以判断必须在加锁之后
pthread_cond_wait(&cond, &lock); // 线程在此处进入等待状态,等待条件变量 cond 发出信号
cout << "Thread " << num << " is running... cnt: " << cnt << endl;
cnt++;
usleep(10000);
pthread_mutex_unlock(&lock);
}
}
int main() {
for (uint64_t i = 1; i <= NUM; i++) {
pthread_t tid;
// 这里的第四个参数,如果想要与新线程共享这个参数的话,可以设为 (void*)&i,进行传址调用
// 我们这里要传值调用,不能让它用 i
pthread_create(&tid, nullptr, Count, (void*)i);
usleep(1000);
}
// 指定唤醒线程来访问临界资源
while (true) {
sleep(1);
pthread_cond_signal(&cond); // 唤醒一个线程
cout << "signal one thread..." << endl;
}
return 0;
}


微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online