
⭐️在这个怀疑的年代,我们依然需要信仰。
个人主页:YYYing.
⭐️Linux/C++进阶 系列专栏:【从零开始的linux/c++进阶编程】
系列上期内容:【Linux/C++多线程篇(一) 】多线程编程入门
系列下期内容:暂无
目录
[📖 创建一个互斥锁](#📖 创建一个互斥锁)
[📖 初始化互斥锁](#📖 初始化互斥锁)
[📖 获取锁资源](#📖 获取锁资源)
[📖 释放锁资源](#📖 释放锁资源)
[📖 销毁互斥锁](#📖 销毁互斥锁)
[📖 创建无名信号量](#📖 创建无名信号量)
[📖 初始化无名信号量](#📖 初始化无名信号量)
[📖 申请无名信号量的资源(P操作)](#📖 申请无名信号量的资源(P操作))
[📖 释放无名信号量的资源(V操作)](#📖 释放无名信号量的资源(V操作))
[📖 销毁无名信号量](#📖 销毁无名信号量)
[📖 创建一个条件变量](#📖 创建一个条件变量)
[📖 初始化条件变量](#📖 初始化条件变量)
[📖 消费者线程进入等待队列](#📖 消费者线程进入等待队列)
[📖 生产者线程唤醒休眠队列中的任务](#📖 生产者线程唤醒休眠队列中的任务)
[📖 销毁条件变量](#📖 销毁条件变量)

前言:当多线程遇上"交通混乱"
想象一下,你是一个繁忙路口的交警,需要同时指挥四面八方的车辆。如果没有红绿灯和交警,所有车辆都凭感觉开,结果必然是撞车、拥堵、混乱。在计算机世界里,多个线程同时访问共享数据时,也会出现类似的"交通事故"------数据错乱、程序崩溃、结果不可预测。
为了让线程们有序地工作,我们需要给它们装上"红绿灯",也就是同步互斥机制。本文将从生活比喻出发,带你轻松理解这些看似复杂的并发工具。
线程的同步互斥机制
一、为什么需要同步互斥?
假设有一个火车票售票系统,剩余票数为 1。两个线程同时执行以下操作
cpp
if (tickets > 0) {
tickets--;
cout << "购票成功";
}
如果两个线程同时检查 tickets > 0,发现都是 1,于是都执行 tickets--,结果票数变成 -1,但两人都以为自己买到了票。这就是典型的竞态条件------程序的结果依赖于线程执行的偶然顺序。
这种问题源于线程的交错执行 :**tickets-- 不是原子操作(**也就是指不会被线程调度机制打断的操作。 ),它实际上分为三步:读取、减一、写回。如果两个线程的步骤交错,就会出错。
要解决这个问题,我们需要确保同一时刻只有一个线程能操作票数 ,这就是互斥 。同时,可能还需要让一个线程等待另一个线程完成某件事(比如等票补足再卖),这就是同步。
总的来说 ------由于同一个进程的多个线程会共享进程的资源,这些被共享的资源称为临界资源, 多个线程对公共资源的抢占问题,访问临界资源的代码段称为临界区, 多个线程抢占进程资源的现象称为竞态, 为了解决竞态,我们引入了同步互斥机制
下面,我们就来看看系统为我们提供的几种"红绿灯"。
二、线程互斥之互斥锁
互斥锁的本质是一个特殊的临界资源,当该临界资源被某个线程所拥有后,其他线程就不能拥有该资源,直到,拥有该资源的线程释放掉互斥锁后,其他线程才能进行抢占(同一时刻,一个互斥锁只 能被一个线程所拥有),相当于一把获取资源的钥匙。
2.1、互斥锁的相关API函数接口
📖 创建一个互斥锁
只需定义一个pthread_mutex_t 类型的变量即创建了一个互斥锁
cpp
pthread_mutex_t mutex;
📖 初始化互斥锁
|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 函数原型 | int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //静态初始化 |
| 头文件 | iostream |
| 功能 | 初始化互斥锁变量 |
| 参数说明 | 参数1:互斥锁变量的地址,属于地址传递 参数2:互斥锁属性,一般填NULL,让系统自动设置互斥锁属性 |
| 返回值 | 成功返回0,失败返回错误码 |
📖 获取锁资源
|----------|------------------------------------------------------|
| 函数原型 | int pthread_mutex_lock(pthread_mutex_t *mutex); |
| 头文件 | iostream |
| 功能 | 获取锁资源,如果要获取的互斥锁已经被其他线程锁定,那么该函数会阻塞,直到能够获取锁资源 |
| 参数说明 | 互斥锁地址,属于地址传递 |
| 返回值 | 成功返回0,失败返回错误码 |
📖 释放锁资源
|----------|--------------------------------------------------------|
| 函数原型 | int pthread_mutex_unlock(pthread_mutex_t *mutex); |
| 头文件 | iostream |
| 功能 | 释放对互斥锁资源的拥有权 |
| 参数说明 | 互斥锁变量的地址 |
| 返回值 | 成功返回0,失败返回错误码 |
📖 销毁互斥锁
|----------|---------------------------------------------------------|
| 函数原型 | int pthread_mutex_destroy(pthread_mutex_t *mutex); |
| 头文件 | iostream |
| 功能 | 销毁互斥锁 |
| 参数说明 | 互斥锁变量的地址 |
| 返回值 | 成功返回0,失败返回错误码 |
2.2、互斥锁的小练习
可以看到我们线程1和线程2都是先抢占我们锁资源然后进行释放,这其中的机制依旧是时间片轮询上下文切换。
cpp
#include<iostream>
#include<cstdio>
#include<cstring>
#include <unistd.h>
using namespace std;
//11、创建一个互斥锁
pthread_mutex_t mutex;
//定义一个全局资源
int num = 520;
//定义分支线程1
void *task1(void *arg){
while(1){
sleep(1);
//临界资源
//33、获取锁资源
pthread_mutex_lock(&mutex);
num -= 10;
//线程1将临界资源减少10
printf("张三取了10,剩余%d\n", num);
//44、释放锁资源
pthread_mutex_unlock(&mutex);
}
}
//定义分支线程2
void *task2(void *arg){
while(1){
sleep(1);
//33、获取锁资源
pthread_mutex_lock(&mutex);
num -= 20;
//线程1将临界资源减少10
printf("李四取了20,剩余%d\n", num);
//44、释放锁资源
pthread_mutex_unlock(&mutex);
}
}
/*****************************主线程****************************/
int main() {
//22、初始化互斥锁,参数NULL表示让系统自动分配互斥锁属性
pthread_mutex_init(&mutex, NULL);
//1、创建两个分支线程
pthread_t tid1,tid2;
if(pthread_create(&tid1, NULL, task1, NULL) != 0){
printf("tid1 create error\n");
return -1;
}
if(pthread_create(&tid2, NULL, task2, NULL) != 0){
printf("tid2 create error\n");
return -1;
}
printf("主线程:tid1 = %#x, tid2 = %#x\n", tid1, tid2);
//2、阻塞等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//55、释放锁资源
pthread_mutex_destroy(&mutex);
std::cout << "Hello, World!" << std::endl;
return 0;
}
三、线程同步之无名信号量
线程同步:就是多个线程之间有先后顺序得执行,这样在访问临界资源时,就不会产生抢占现象了
同步机制常用于生产者消费者模型:消费者任务要想执行,必须先执行生产者线程,多个任务有顺序执行
无名信号量:本质上也是一个特殊的临界资源,内部维护了一个value值,当某个进行想要执行之前,先申请该无名信号量的value资源,如果value值大于0,则申请资源函数接触阻塞,继续执行后续操作。如果value值为0,则当前申请资源函数会处于阻塞状态,直到其他线程将该value值增加到大于0
3.1、无名信号量的相关API函数接口
📖 创建无名信号量
只需定义一个sem_t 类型的变量即可
cpp
sem_t sem;
📖 初始化无名信号量
|----------|-------------------------------------------------------------------------------------------------|
| 函数原型 | int sem_init(sem_t *sem, int pshared, unsigned int value); |
| 头文件 | semaphore.h |
| 功能 | 初始化无名信号量,最主要是初始化value值 |
| 参数说明 | 参数1:无名信号量的地址 参数2:判断进程还是线程的同步 0:表示线程间同步 非0:表示进程间同步,需要创建在共享内存段中 参数3:无名信号量的初始值 |
| 返回值 | 成功返回0,失败返回-1并置位错误码 |
📖 申请无名信号量的资源(P操作)
|----------|-------------------------------------------------------------------|
| 函数原型 | int sem_wait(sem_t *sem); |
| 头文件 | semaphore.h |
| 功能 | 阻塞申请无名信号量中的资源,成功申请后,会将无名信号量的value进行减1操作,如果当前无名信号量的value为0,则阻塞 |
| 参数说明 | 无名信号量的地址 |
| 返回值 | 成功返回0,失败返回-1并置位错误码 |
📖 释放无名信号量的资源(V操作)
|----------|--------------------------------|
| 函数原型 | int sem_post(sem_t *sem); |
| 头文件 | semaphore.h |
| 功能 | 将无名信号量的value值增加1操作 |
| 参数说明 | 无名信号量的地址 |
| 返回值 | 成功返回0,失败返回-1并置位错误码 |
📖 销毁无名信号量
|----------|-----------------------------------|
| 函数原型 | int sem_destroy(sem_t *sem); |
| 头文件 | semaphore.h |
| 功能 | 销毁无名信号量 |
| 参数说明 | 无名信号量的地址 |
| 返回值 | 成功返回0,失败返回-1并置位错误码 |
3.2、互斥锁的小练习
我们现在再来看看同步在生产者消费者模型中的应用,当 进程2 想要执行之前,先申请无名信号量的value资源,如果value值大于0,则申请资源函数解除阻塞,并继续执行后续操作。如果value值为0,则当前申请资源函数会处于阻塞状态,直到 线程1 将该value值增加到大于0
cpp
#include<iostream>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<semaphore.h>
#include<pthread.h>
sem_t sem;
//创建生产者线程
void *task1(void *arg){
int num = 5;
while(num--){
sleep(1);
printf("我生产了一辆特斯拉\n");
//44、释放无名信号量资源
sem_post(&sem);
}
//退出线程
pthread_exit(NULL);
}
//创建消费者线程
void *task2(void *arg){
int num = 5;
while(num--){
//33、申请无名信号量的资源
sem_wait(&sem);
printf("我消费了一辆特斯拉,很开心\n");
}
//退出线程
pthread_exit(NULL);
}
/*******************************主程序*************************/
int main() {
//22、初始化无名信号量,第一个0表示用于线程间通信,第二个0表示初始值为0
sem_init(&sem, 0, 0);
//1、创建两个分支线程
pthread_t tid1,tid2;
if(pthread_create(&tid1, NULL, task1, NULL) != 0){
printf("tid1 create error\n");
return -1;
}
if(pthread_create(&tid2, NULL, task2, NULL) != 0){
printf("tid2 create error\n");
return -1;
}
printf("主线程:tid1 = %#x, tid2 = %#x\n", tid1, tid2);
//2、阻塞等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//55、销毁无名信号量
sem_destroy(&sem);
return 0;
}
四、线程同步之条件变量
我们不难发现,如果我们只用互斥锁,那么我们的消费者和生产者只能是一对一的关系,但我们要想让一个生产者对应多个消费者用互斥锁就不行了,所以我们就要用到我们的条件变量了。
条件变量本质上也是一个临界资源,他维护了一个队列,当消费者线程想要执行时,先进入队列中等待生产者的唤醒。执行完生产者,再由生产者唤醒在队列中的消费者,这样就完成了生产者和消费者之间的同步关系。
但是,多个消费者在进入休眠队列的过程是互斥的,所以,在消费者准备进入休眠队列时,我们需要使用互斥锁来进行互斥操作。
4.1、条件变量的API函数接口
📖 创建一个条件变量
只需定义一个pthread_cond_t类型的全局变量即可
cpp
pthread_cond_t cond;
📖 初始化条件变量
|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 函数原型 | int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //静态初始化 |
| 头文件 | iostream |
| 功能 | 初始化条件变量 |
| 参数说明 | 参数1:条件变量的起始地址 参数2:条件变量的属性,一般填NULL |
| 返回值 | 成功返回0,失败返回一个错误码 |
📖 消费者线程进入等待队列
|----------|---------------------------------------------------------------------------------------------|
| 函数原型 | int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); |
| 头文件 | iostream |
| 功能 | 将线程放入休眠等待队列,等待其他线程的唤醒 |
| 参数说明 | 参数1:条件变量的地址 参数2:互斥锁,由于多个消费者线程进入等待队列时会产生竞态,为了解决竞态,需要使用一个互斥锁 |
| 返回值 | 成功返回0,失败返回错误码 |
📖 生产者线程唤醒休眠队列中的任务
|----------|--------------------------------------------------------|-----------------------------------------------------|
| 函数原型 | int pthread_cond_broadcast(pthread_cond_t *cond); | int pthread_cond_signal(pthread_cond_t *cond); |
| 头文件 | iostream | iostream |
| 功能 | 唤醒条件变量维护的队列中的所有消费者线程 | 唤醒条件变量维护的队列中的第一个进入队列的消费者线程 |
| 参数说明 | 条件变量的地址 | 条件变量的地址 |
| 返回值 | 成功返回0,失败返回错误码 | 成功返回0,失败返回错误码 |
📖 销毁条件变量
|----------|------------------------------------------------------|
| 函数原型 | int pthread_cond_destroy(pthread_cond_t *cond); |
| 头文件 | iostream |
| 功能 | 销毁一个条件变量 |
| 参数说明 | 条件变量的地址 |
| 返回值 | 成功返回0,失败返回错误码 |
3.2、条件变量的小练习
我们再以生产者消费者模型来看看我们的用法,我们既可以一个一个唤醒,也可以直接一次性将消费者全部唤醒。
cpp
#include<iostream>
#include<unistd.h>
#include<pthread.h>
//11、定义一个条件变量
pthread_cond_t cond;
//111、定义一个互斥锁
pthread_mutex_t mutex;
//创建生产者线程
void *task1(void *arg)
{
/*
int num = 3;
while(num--)
{
sleep(1);
printf("%#x:生产了一辆特斯拉\n", pthread_self());
//44、唤醒一个消费者进行消费
pthread_cond_signal(&cond);
}
*/
sleep(3);
printf("我生产了3辆特斯拉\n");
//44、唤醒所有消费者线程
pthread_cond_broadcast(&cond);
//退出线程
pthread_exit(NULL);
}
//创建消费者线程
void *task2(void *arg)
{
//333、获取锁资源
pthread_mutex_lock(&mutex);
//33、进入休眠队列,等待生产者的唤醒
pthread_cond_wait(&cond, &mutex);
printf("%#x:消费了一辆特斯拉,很开心\n", pthread_self());
//444、释放锁资源
pthread_mutex_unlock(&mutex);
//退出线程
pthread_exit(NULL);
}
int main() {
//22、初始化条件变量
pthread_cond_init(&cond, NULL);
//222、初始化互斥锁
pthread_mutex_init(&mutex, NULL);
//1、创建两个分支线程
pthread_t tid1,tid2,tid3,tid4;
if(pthread_create(&tid1, NULL, task1, NULL) != 0){
printf("tid1 create error\n");
return -1;
}
if(pthread_create(&tid2, NULL, task2, NULL) != 0){
printf("tid2 create error\n");
return -1;
}
if(pthread_create(&tid3, NULL, task2, NULL) != 0){
printf("tid3 create error\n");
return -1;
}
if(pthread_create(&tid4, NULL, task2, NULL) != 0){
printf("tid4 create error\n");
return -1;
}
printf("主线程:tid1 = %#x, tid2 = %#x, tid3 = %#x, tid4 = %#x\n", tid1, tid2, tid3, tid4);
//2、阻塞等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
pthread_join(tid4, NULL);
//55、销毁条件变量
pthread_cond_destroy(&cond);
///555、销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
C++11中的多线程
C++11之后就支持线程支持库了,也支持线程创建、互斥锁、条件变量,线程支持库需要引入头文件 #include<thread>
一、线程相关常用操作
1.1、线程的创建
C++线程支持库,本质是是面向对象的操作,可以使用构造函数完成。

1.2、线程体函数种类
-
可以是任意类型的函数,不必要是 void * 类型参数也是void *类型
-
可以是全局函数,也可以是类中成员函数当做线程体
-
可以是仿函数当作线程体函数
-
也可以是Lambda表达式当作线程体函数
1.3、线程号获取
cpp
this_thread::get_id()

下述代码为我们展示了4种不同的线程体函数。
cpp
#include<iostream>
#include <thread>
using namespace std;
/*****************第一个测试线程体*******************/
void ThreadFun_1(){
cout<<"ThreadFun_1 tid = "<< this_thread::get_id()<<endl;
cout << "ThreadFun_1 test"<<endl;
}
/*****************第二个线程体测试**********************/
void ThreadFun_2(int num, string str){ //有参无返回值函数
cout<<"ThreadFun_2 tid = "<< this_thread::get_id()<<endl;
cout<<"num = " << num << " str = " << str <<endl;
}
/*******************第三个线程体测试*********************/
class ThreadClass{
public:
string name;
int age;
void ThreadClassFun(){ //类中成员函数作为线程体函数
cout<<"ThreadFun_3 tid = "<< this_thread::get_id()<<endl;
cout << "name = "<<this->name<<" age = "<<age<<endl;
}
};
/**********************主程序**********************/
int main(int argc, const char *argv[]){
//主程序就是主线程
//创建第一个分支线程,使用无参函数完成线程体为无参无返回值函数
thread th1(ThreadFun_1);
//创建第二个分支线程,并向线程体中传递数据
string name = "zpp";
thread th2(ThreadFun_2, 520, name);
//此时就创建了一个分支线程,线程体函数
//可以直接向线程体传递参数,有多少可以传多少,无需使用结构体完成
//创建第三个分支线程,向线程体中传递一个类的成员函数
ThreadClass test;
test.name = "zhangsan";
test.age = 18;
thread th3(&ThreadClass::ThreadClassFun, &test);
/************lambda表达式当作线程体:开发过程中用的比较多的************/
//创建第四个分支线程,将lambda表达式当作线程体函数
thread th4([](int key){
cout<<"ThreadFun_4 tid = "<< this_thread::get_id()<<endl;
cout<<"key = "<<key<<endl;
}, 999);
//阻塞回收分支线程
th1.join();
th2.join();
th3.join();
th4.join();
return 0;
}
1.4、线程号回收
- 阻塞方式回收线程
cpp
th1.join();
- 非阻塞方式回收线程
cpp
th1.detach();

此处的阻塞非阻塞回收与我们上一篇讲的并无二异,说到底,这些类的实现本质也是在用我们上节课c语言的那些函数所做出来的。
cpp
#include<iostream>
#include<thread>
using namespace std;
/*****************第一个测试线程体*******************/
void ThreadFun_1(){ // 无参无返回值
for (int i = 0; i < 10; i++){
cout << "ThreadFun_1 tid = " << this_thread::get_id() << endl;
cout << "ThreadFun_1 test" << endl;
//延时函数
this_thread::sleep_for(1s); //等待1秒时间
}
}
/**********************主程序**********************/
int main(int argc, const char *argv[]){
// 阻塞回收分支线程
//th1.join();
// 主程序就是主线程
// 创建第一个分支线程,使用无参函数完成线程体
thread th1(ThreadFun_1); // 此时就创建了一个分支线程,线程体函数为无参无返回值函数
th1.detach(); //将线程设置成分离态:主线程可以继续做自己其他事情
this_thread::sleep_for(20s);
return 0;
}
二、互斥锁的使用
互斥锁本质上是完成将多个线程使用临界资源时,防止竞态
我们需要在c++编程中需要引入头文件 #include<mutex>

2.1、常用函数
cpp
1、构造函数:创建一个互斥锁对象
2、 lock():上锁
3、 unlock():释放锁资源

2.2、lock_guard的使用
但在这其中,我们的mutex互斥锁经常会与lock_guard进行一起使用,用于在其构造时自动获取锁,在析构时自动释放锁。使用 std::lock_guard 的好处是,当 std::lock_guard 对象离开其作用域时,会自动调用析构函数,该析构函数会释放锁。这确保了在任何情况下(包括由于异常等原因导致的提前退出),锁都会被正确释放,从而避免了忘记手动释放锁而导致的死锁问题。
cpp
std::mutex myMutex;
std::lock_guard<std::mutex> lock(myMutex);

2.3、代码演示
cpp
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex mux; //实例化一个互斥锁
/*****************第一个测试线程体*******************/
void ThreadFun_1(){ // 无参无返回值
mux.lock(); //获取锁资源
cout<<"======================================"<<endl;
cout<<"tid = "<<this_thread::get_id()<<endl;
this_thread::sleep_for(1s);
cout<<"**************************************"<<endl;
mux.unlock(); //释放锁资源
}
/**********************主程序**********************/
int main(int argc, const char *argv[]){
for(int i=0; i<10; i++){
thread th(ThreadFun_1);
th.detach();
}
//线程分离
this_thread::sleep_for(20s);
std::cout << "Hello, World!" << std::endl;
this_thread::sleep_for(20s);
return 0;
}
三、条件变量
实现一个生产者对应多个消费者问题,需要引入头文件:#include<condition_variable>

2.1、常用函数
cpp
1、 构造函数:创建并初始化一个条件变量
2、 wait():将消费者线程放入等待队列中
3、 唤醒线程:
cv.notify_one(); 唤醒一个线程
cv.notify_all(); 唤醒所有线程

2.2、代码演示
不难看出与我们刚才所讲的同步机制中的条件变量逻辑是非常相似的。
cpp
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
using namespace std;
//定义一个条件变量
condition_variable cv;
mutex mux;
//条件变量头文件
//线程支持库头文件
//互斥锁
//用于防止竞态的互斥锁
//定义生产者线程
void ThreadWrite(){
for(int i=0; i<5; i++){
this_thread::sleep_for(2s);
cout<<"我生产了一辆特斯拉"<<endl;
cv.notify_one(); //通知一个线程可以消费了
//通知所有线程
//cv.notify_all();
}
}
//定义消费者线程
void ThreadRead(){
//提前先进入消费者队列
unique_lock<mutex> lock(mux);
cv.wait(lock);
cout<<"我消费了一辆特斯拉"<<endl;
lock.unlock(); // 解锁
}
int main(int argc, const char *argv[]) {
//创建生产者线程
thread th1(ThreadWrite);
//每隔两秒时间生产一辆特斯拉
//创建多个消费之
for(int i=0; i<5; i++){
thread th2(ThreadRead);
th2.detach();
}
th1.join(); //阻塞回收线程
return 0;
}
结语
当你掌握了"红绿灯"和"独木桥"的原理,你就已经跨过了多线程编程最危险的那道门槛。接下来,就是去享受多核 CPU 带来的速度激情吧!
我是YYYing, 后面还有更精彩的内容,希望各位能多多关注支持一下主包。
无限进步,我们下次再见!
---⭐️ 封面自取 ⭐️---
