【Linux】Linux 操作系统 - 33 , 线程(二) 线程互斥和同步 , 带你对线程使用深刻理解 !
文章目录
- ● 需要知道的知识点
- ● 线程互斥
- 一 、是什么 ?
- 二 、什么叫做数据竞争 , 数据不一致 ?
- 三 、互斥锁(互斥量) (掌握)
- 1 . 介绍
- 2 . 锁相关接口
- 2.1 创建锁和销毁锁
- 2.2 解锁和解锁
- 2.3 互斥锁正确使用步骤
- 3 . 互斥锁的基本使用
- 3.1 全局锁的使用
- 3.2 局部锁的使用
- 4 . 互斥锁的原理 (掌握)
- 总结好了 (面试)
- ● 线程同步
- 一 、线程同步是什么 ?
- 二 、条件变量
- 1 . 是什么 ?
- 2 . 使用 - 相关接口
- 2.1 创建和销毁
- 2.2 等待和唤醒
- 2.3 使用步骤
- 3 . 条件变量使用注意事项(特别重要 , 要注意 !!!!!)
- 面试回答 : 为什么条件变量必须使用 while 循环 ?
- 面试回答 : 了解 pthread_cond_wait 吗 ? 为什么参数要用锁 ??
- 4 . 条件变量的使用
- 三 、POSIX信号量
- 1 . 是什么 ?
- 2 . 理解信号量
- 3 . 多线程使用资源分类 (重要)
- 4 . 信号量的相关接口
- 4.1 创建和销毁
- 4.2 等待(P操作)和唤醒(V操作)
- 5 . 信号量的基本使用
- ● 线程互斥和同步的区别 , 条件变量和信号量的区别 (面试可能考)
- 思维导图总结
● 需要知道的知识点

● 线程互斥
一 、是什么 ?
线程互斥(Thread Mutual Exclusion)是多线程编程中的一个重要概念,用于解决多个线程同时访问共享资源时可能引发的数据竞争(Data Race)和不一致性问题的一种解决方案 。
二 、什么叫做数据竞争 , 数据不一致 ?
在线程中 , 我们可能创建多个线程 , 但是多个线程访问了共同资源 , 多个线程就是多个执行流 , 但资源只有一份 , 所以多个线程就要竞争 , 在竞争过程中 , 可能就会引发数据不一致问题 ! 以下例子来具体理解 :
#include <iostream>
#include <pthread.h>
#include <string>
#include <vector>
#include <unistd.h>
//////// 演示什么是数据不一致 !int gticksts = 1000; // 总票数// 这个是抢票 , 票数为 1000 , 多个线程来执行
void *Tickets(void *args)
{std::string name = static_cast<const char *>(args);while (1){if (gticksts > 0){usleep(1000); // 1000 ms std::cout << name << "sells ticket : " << gticksts << std::endl;gticksts--;}else{break;}}return nullptr;
}int main()
{std::vector<pthread_t> threads;// 创建5个线程for (int i = 1; i <= 5; ++i){pthread_t tid;char *name = new char[64]; // 开辟一个空间snprintf(name, 64, "thread-%d", i);pthread_create(&tid, nullptr, Tickets, name);threads.emplace_back(tid);}//等待 5 个线程 for(auto& tid : threads){pthread_join(tid , nullptr);}return 0;
}


- 理解
也就是说 : 当前我们的代码是允许多个执行流并行执行的 , 即 : 你在执行的时候 , 我也在执行 , 执行不是原子的 .


所以 , 对于共享资源 , 我们要对共享资源进行保护 , 这样就会解决数据不一致的问题 , 本质就是 : 只允许一个执行流来执行就行了 , 其它执行流不要影响我 !
三 、互斥锁(互斥量) (掌握)
这个就是解决数据不一致问题的一个重要的方法 !
1 . 介绍

2 . 锁相关接口
2.1 创建锁和销毁锁

2.2 解锁和解锁

2.3 互斥锁正确使用步骤

3 . 互斥锁的基本使用
用互斥锁解决一下 , 以上抢票数据不一致的问题 !
3.1 全局锁的使用
#include <iostream>
#include <pthread.h>
#include <string>
#include <vector>
#include <unistd.h>
//////// 演示什么是数据不一致 !int gticksts = 1000; // 总票数// 1. 定义一个全局的锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // INITIALIZER - initiallizer 初始化// 这个是抢票 , 票数为 1000 , 多个线程来执行
void *Tickets(void *args)
{std::string name = static_cast<const char *>(args);while (1){// 这里是临界区的开始 , 因为里面访问了共享资源// 这里加锁pthread_mutex_lock(&mutex);if (gticksts > 0){usleep(1000); // 1000 msstd::cout << name << " sells ticket : " << gticksts << std::endl;gticksts--;// 这里解锁pthread_mutex_unlock(&mutex);} // 到这里临界区的结束 ..else{// 这里必须解锁 !!!!!!!!!!!!! 防止出现死锁 , 当票数 = 0 时会走这里pthread_mutex_unlock(&mutex);break;}}return nullptr;
}int main()
{std::vector<pthread_t> threads;// 创建5个线程for (int i = 1; i <= 5; ++i){pthread_t tid;char *name = new char[64]; // 开辟一个空间snprintf(name, 64, "thread-%d", i);int n = pthread_create(&tid, nullptr, Tickets, name);if (n == 0){std::cout << "Create " << name << " Success ! " << std::endl;threads.emplace_back(tid);}}// 等待 5 个线程for (auto &tid : threads){int n = pthread_join(tid, nullptr);if (n == 0){std::cout << " Join Success ! " << std::endl;}}return 0;
}

3.2 局部锁的使用
#include <iostream>
#include <pthread.h>
#include <string>
#include <vector>
#include <unistd.h>
//////// 演示什么是数据不一致 !int gticksts = 1000; // 总票数// 2. 定义了一个局部锁
pthread_mutex_t mutex;// 这个是抢票 , 票数为 1000 , 多个线程来执行
void *Tickets(void *args)
{std::string name = static_cast<const char *>(args);while (1){// 这里是临界区的开始 , 因为里面访问了共享资源// 这里加锁pthread_mutex_lock(&mutex);if (gticksts > 0){usleep(1000); // 1000 msstd::cout << name << " sells ticket : " << gticksts << std::endl;gticksts--;// 这里解锁pthread_mutex_unlock(&mutex);} // 到这里临界区的结束 ..else{// 这里必须解锁 !!!!!!!!!!!!! 防止出现死锁 , 当票数 = 0 时会走这里pthread_mutex_unlock(&mutex);break;}}return nullptr;
}int main()
{std::vector<pthread_t> threads;// 局部锁初始化pthread_mutex_init(&mutex, nullptr);// 创建5个线程for (int i = 1; i <= 5; ++i){pthread_t tid;char *name = new char[64]; // 开辟一个空间snprintf(name, 64, "thread-%d", i);int n = pthread_create(&tid, nullptr, Tickets, name);if (n == 0){std::cout << "Create " << name << " Success ! " << std::endl;threads.emplace_back(tid);}}// 等待 5 个线程for (auto &tid : threads){int n = pthread_join(tid, nullptr);if (n == 0){std::cout << " Join Success ! " << std::endl;}}// 2. 定义局部锁时 :// 这里销毁锁pthread_mutex_destroy(&mutex);return 0;
}

4 . 互斥锁的原理 (掌握)

总结好了 (面试)

● 线程同步
考虑一个问题 , 单纯的线程互斥有问题吗 ? 线程互斥是解决数据不一致问题的 , 但是对多个线程来说有什么缺点 ??

所以 , 单纯的互斥是有缺陷的 , 为了解决这样的缺陷 , 引出了同步的话题 !
一 、线程同步是什么 ?
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步 。
- 什么叫做饥饿呢 ??
以上只有互斥的例子 , 在多个线程运行时 , 只有一些线程能够得到锁 , 访问临界资源 ,但始终有一些线程没有机会得到资源 , 这样的线程叫 : 饥饿线程 。这是不公平的 .
然而 , 同步就是让所有线程的访问有序的进行 , 均可访问临界资源 , 谁排在前面谁先访问 , 所以可以有效的解决饥饿问题 。
二 、条件变量
条件变量是实现同步的一种方式 !
1 . 是什么 ?
- 互斥时 , 一个线程竞争锁成功 , 该线程访问临界区 , 进行相关操作 , 但是其余线程就要一直阻塞等待直到锁的释放 , 什么事情都做不了 .
- 条件变量 : 特点就是允许竞争到锁的线程唤醒其它等待的线程 .
概念 :
条件变量是一种线程同步机制,用于在线程间传递 “某个条件是否满足” 的信号。它允许线程在条件不满足时主动暂停执行(释放锁并进入等待状态),直到其他线程修改条件并显式唤醒它。条件变量中是有等待队列的 .
2 . 使用 - 相关接口
2.1 创建和销毁

2.2 等待和唤醒
该接口就能体现出条件变量的特性了 !


2.3 使用步骤

3 . 条件变量使用注意事项(特别重要 , 要注意 !!!)
//////////// 线程等待
// 错误示例:使用if检查条件可能导致程序崩溃
pthread_mutex_lock(&mutex);
if (buffer_empty()) { // 检查缓冲区是否为空pthread_cond_wait(&cond, &mutex); // 若为空则等待
}
// 风险点:若被虚假唤醒或条件已变化,会直接执行后续代码
consume_data(); // 可能访问空缓冲区,导致程序崩溃
pthread_mutex_unlock(&mutex);//////////// 线程唤醒
...... 这里唤醒该条件变量下的一个线程pthread_cond_signal(&cond);
- 为什么必须使用 while 循环 ? (详细解释)

面试回答 : 为什么条件变量必须使用 while 循环 ?

面试回答 : 了解 pthread_cond_wait 吗 ? 为什么参数要用锁 ??

4 . 条件变量的使用
// 条件变量的使用练习
// 条件变量是实现同步机制的 -- 让多线程有顺序的访问
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <vector>
#include <cstdio>// 全局计数器
int counter = 0; // 全局计数器
#define TARGET_COUNT 5 // 目标计数
// 全局锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//////////////////// 1. 全局的条件变量 ////////////////////
// pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//////////////////// 2. 局部的条件变量 ////////////////////pthread_cond_t cond;// 工作线程:增加计数器
void *worker(void *args)
{std::string name = static_cast<char *>(args);for (int i = 0; i < TARGET_COUNT; i++){pthread_mutex_lock(&mutex);// 增加计数器counter++;std::cout << "工作线程 : " << name << " - > 计数器增加到 : " << counter << std::endl;// 当计数器达到目标值时,通知等待的线程if (counter == TARGET_COUNT){// 唤醒该条件变量下的一个线程pthread_cond_signal(&cond);std::cout << " 工作线程: 已发出通知,计数器达到目标值 !" << std::endl;}pthread_mutex_unlock(&mutex);// 短暂休眠,模拟工作耗时usleep(100000);}return nullptr;
}// 主线程
int main()
{//////////////////// 2. 局部的条件变量 ////////////////////pthread_cond_init(&cond, nullptr);std::vector<pthread_t> threads;// 创建5个线程for (int i = 1; i <= 5; ++i){pthread_t tid;char *name = new char[64]; // 开辟一个空间snprintf(name, 64, "thread-%d", i);int n = pthread_create(&tid, nullptr, worker, name);if (n == 0){threads.emplace_back(tid);}}// 等待 5 个线程for (auto &tid : threads){int n = pthread_join(tid, nullptr);if (n == 0){std::cout << " Join Success ! " << std::endl;}}// 主线程等待计数器达到目标值pthread_mutex_lock(&mutex);while (counter < TARGET_COUNT){printf("主线程: 等待计数器达到 %d...\n", TARGET_COUNT);pthread_cond_wait(&cond, &mutex);printf("主线程: 被唤醒,当前计数器值为 %d\n", counter);}pthread_mutex_unlock(&mutex);pthread_cond_destroy(&cond);return 0;
}

三 、POSIX信号量
信号量是实现同步的一种方式 !
1 . 是什么 ?
信号量是实现同步机制的一种方式,它通过一个受保护的整数变量及相关的等待队列,结合 P 操作(等待,将信号量值减 1,若值为负则阻塞调用者)和 V 操作(信号,将信号量值加 1,若有阻塞线程则唤醒其一),实现对共享资源的有序访问控制。
信号量的两个核心操作 :
-
P 操作 – 等待资源
该操作是对信号量的值--, 若结果为负,调用者阻塞等待 。该操作必须是原子的 , 在执行过程中不允许被其它线程打断 。 -
V 操作 – 唤醒资源
该操作是对信号量的值++, 若有阻塞线程,唤醒其中一个。该操作必须是原子的 , 在执行过程中不允许被其它线程打断 。
2 . 理解信号量

3 . 多线程使用资源分类 (重要)


4 . 信号量的相关接口
信号量也是 pthread 库封装的 , 所以编译时要链接该库 !
4.1 创建和销毁

4.2 等待(P操作)和唤醒(V操作)

5 . 信号量的基本使用
// 信号量接口的使用#include <iostream>
#include <pthread.h>
#include <string>
#include <semaphore.h>
#include <unistd.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
sem_t sem; // 信号量 , 主新线程共享void *work(void *args)
{// 为主线程创建信号量 ,std::string name = static_cast<const char *>(args);int cnt = 5;while (cnt--){// 消耗资源 -- P操作 , --信号量sem_wait(&sem);pthread_mutex_lock(&mutex);std::cout << "我是新线程 : " << name << " 我消耗了一个资源 !" << std::endl;pthread_mutex_unlock(&mutex);sleep(2); // 我每隔2s拿一个}return nullptr;
}int main()
{// 考虑一个问题 , 是先创建线程呢 ?? 还是先创建信号量呢 ??// 先创建信号量 , 因为信号量是表示资源的多少 , 表示有几个线程访问资源// 所以先创建信号量 , 表示有线程就就绪了 , 如果信号量都创建失败了 , 线程就没有必要创建了// 初始化信号量 , 刚开始没有资源 , 所以为 : 0sem_init(&sem, 0, 0);// 创建一个线程pthread_t tid;pthread_create(&tid, nullptr, work, (void *)"thread-1");// 主线程// 主线程放资源 , P操作int cnt = 5;while (cnt--){sleep(1); // 每隔 1s 放一个资源// V操作 , ++ 信号量 , 所以放资源sem_post(&sem);pthread_mutex_lock(&mutex);std::cout << " 我是主线程 , 我放了一个资源 , 等待新线程消耗 .. " << std::endl;pthread_mutex_unlock(&mutex);}// 等待线程pthread_join(tid, nullptr);sem_destroy(&sem);return 0;
}
● 线程互斥和同步的区别 , 条件变量和信号量的区别 (面试可能考)
总结好了 :

思维导图总结

