👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 一、同步的相关概念
- 二、同步的相关操作
-
-
- [2.1 条件变量](#2.1 条件变量)
- [2.2 条件变量初始化](#2.2 条件变量初始化)
- [2.3 销毁条件变量](#2.3 销毁条件变量)
- [2.4 条件等待](#2.4 条件等待)
- [2.5 唤醒线程](#2.5 唤醒线程)
- [2.6 简单使用线程同步相关接口](#2.6 简单使用线程同步相关接口)
-
- 三、代码
一、同步的相关概念
- 同步:在保证数据安全的前提下,即同步必须要配合互斥锁来使用,然后让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题。
- 饥饿问题:执行流被永久地阻塞,无法继续执行下去,尽管它们可能处于一个可执行状态。
为了加深概念的理解,举一个生活上的例子:
- 有一个无人监管
VVIP
自习室,只能有一个人在里面学习。所以,为了保证任何时刻只能有一个人进来,大家都要遵守一个规则:门口有一把锁,谁获得这个锁资源,谁就有资格在里面学习。- 有一天你起的很早,当你来到自习室门口发现门旁有锁,于是你就把锁带进去了。当下一个同学来的时候,发现门旁没有锁,就只能在外面等待。突然某一个时刻,你饿了,想去食堂干饭。你刚刚出门准备把锁放回门旁,发现门外
50m
有一堆同学正在等待(竞争),为了能独享这个自习室,你就赶快拿回锁回到自习室了,然后在自习室从早到晚。 那么自习室外的这批人因为长时间得不到锁资源,导致了饥饿问题。
因此,在多线程环境中,如果某个线程长时间占用了资源,其他线程可能因为无法获得资源而长时间等待,甚至导致饥饿问题。
这种情况下,为了公平和效率,需要考虑资源分配的策略。因此,学校再次规定:
- 自习室外面的同学(线程)必须排队。
- 出来的人不能立马重新申请锁资源,必须排到队列的尾部。
所以解决饥饿问题的关键是:在安全的规则下,让所有同学(线程)获取锁资源按照一定的顺序性,我们称为同步 。即解决饥饿问题的方法是线程同步。
二、同步的相关操作
2.1 条件变量
在线程互斥章节,我们使用了互斥锁确保了对共享资源的安全访问,即保证了数据一致性。但是并不能保证线程并发访问的顺序。这是因为线程竞争锁资源时,谁先获得锁是不确定的,取决于操作系统的调度和线程的执行情况。
cpp
#include <unistd.h>
#include <iostream>
#include <pthread.h>
using namespace std;
const int NUM = 5; // 线程个数
int count = 0; // 临界资源
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 创建锁资源
void *thread_task(void *args)
{
// 线程分离:让操作系统回收线程的资源
pthread_detach(pthread_self());
// 我的Linux机器默认是64位,指针是8字节,因此我使用8字节的long long
// 而不是用int是因为会发生截断。
long long number = (long long)args; // 线程编号1~5
cout << "pthread" << number << "创建成功..." << endl;
while (true)
{
// 加锁
pthread_mutex_lock(&lock);
cout << "pthread" << number << ": count = " << ++count << endl;
// 解锁
pthread_mutex_unlock(&lock);
}
}
int main()
{
for (long long i = 1; i <= NUM; i++)
{
pthread_t tid;
pthread_create(&tid, nullptr, thread_task, (void *)i);
// 通过适当的休眠,可以减少线程之间的竞争条件,
// 即减少因多个线程同时被创建而可能导致的竞争和不确定性。
sleep(1);
}
sleep(3); // 让所有线程都去等待队列中等待
cout << "所有线程全部入队列" << endl;
// 主线程不要退出,不然该进程下的所有线程都退出了
while (true)
{
sleep(1);
}
return 0;
}
【程序结果】
如上,一直是pthread1
使用临界资源,导致其它线程出现饥饿问题。因此,Linux
中的原生线程库pthread
中提供了条件变量来实现线程同步。即解决了饥饿问题,也保证了线程依次按顺序获取锁资源。
- 条件变量:是一种用于等待的同步机制,可以实现线程间通信,它必须与互斥锁配合使用,等待某个条件的发生,然后线程才能继续执行 。它通常由两部分组成:
-
线程等待队列:条件变量包含一个等待队列,用于存放因资源不就绪而被阻塞的线程。
-
通知机制:条件变量还包含一个用于表示资源是否就绪的状态。当条件就绪时,等待队列的线程可以被唤醒并继续执行。
-
说明:条件变量也是要被线程库管理 起来的。因为多个线程都可以通过线程库来申请条件变量,当然还有锁资源。诸如线程未来要去哪个等待队列中排队?这总得区分吧。因此,未来只要需要创建多个的东西,必定是需要进行管理的,即先描述,再组织。一般通过结构体来描述
struct
,所以条件变量也必定是一个结构体(包括锁资源)。
-
具体而言,所有线程想进入临界区访问临界资源时,都会先尝试获取互斥锁。如果临界资源未就绪,那么该线程会将自己放入条件变量的等待队列的队尾中。一旦线程在等待队列中等待时条件变量的条件得到满足,即临界资源就绪,那么该线程就会被唤醒继续执行。
2.2 条件变量初始化
作为出自原生线程库的条件变量,使用接口与互斥锁风格差不多,比如互斥锁的数据类型为pthread_mutex_t
,而条件变量的类型数据类型为pthread_cond_t
,这个两个数据类型都是库为用户提供的,直接使用就行。
条件变量初始化也有两种方法:
- 方法一:静态分配。
直接定义在全局 ,并且可以直接通过初始化变量来完成。它是通过使用PTHREAD_COND_INITIALIZER
宏来静态初始化一个条件变量对象。
c
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
注意:对于静态分配的条件变量对象,是不需要显式调用pthread_cond_destroy
函数来销毁它。其静态分配的对象会在程序结束时自动释放其资源,因为它们的生命周期与程序的生命周期相同。
- 方法二:动态分配。需要通过
pthread_cond_init
函数来进行初始化。
c
#include <pthread.h>
// 定义一个条件变量
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond
, const pthread_condattr_t *restrict attr);
说明:
-
cond
:指向要初始化的条件变量的指针。在调用pthread_cond_init
前,必须确保cond
是一个未初始化的条件变量。 -
attr
:指向条件变量属性的指针。直接传入nullptr
即可,表示使用默认属性。 -
返回值:成功返回
0
;失败返回非零错误码,具体的错误码可以用 errno 来获取具体的错误信息。
2.3 销毁条件变量
c
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
说明:
cond
是一个指向条件变量的指针,指向要销毁的条件变量对象。- 返回值:销毁成功返回
0
,失败返回非0
的错误码。具体的错误码可以用errno
来获取具体的错误信息。
注意:条件变量在销毁前,必须确保不再被任何线程使用,否则会导致未定义的行为。
2.4 条件等待
当线程访问某种资源,如果发现该资源没有就绪,就要让该线程在等待队列中排队,等待资源就绪。
c
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
说明:
-
cond
:这是一个指向条件变量的指针。条件变量用于线程之间的同步,允许一个或多个线程等待某个条件成立而被阻塞。 -
mutex
:这是一个指向互斥锁的指针。主要是确保了每个线程看到的是同一个互斥锁资源。如果不是同一个互斥锁,不同线程可能会用不同的锁来保护共享资源,这样就无法确保线程间的互斥访问,会导致数据竞争和不确定行为。而同一把互斥锁确保了所有线程在访问共享资源时的互斥性,从而避免了并发访问问题。 -
返回值:销毁成功返回
0
,失败返回非0
的错误码。具体的错误码可以用errno
来获取具体的错误信息。
这个函数的怎么用是有讲究的
我们必须要想明白以下问题:
- 为什么要使用条件等待 --- 不就是临界资源不就绪。没错,临界资源也要状态。
- 我们怎么知道临界资源是就绪还是不就绪?一定是程序员判断出来的。而判断也是变相访问临界资源,也就是判断必须在加锁和解锁之间,即条件等待要在加锁和解锁之间使用。
因此,每一个线程都需要先尝试获取锁资源之后,即先调用函数pthread_mutex_lock
让每个线程获取锁资源,再通过条件变量判断条件是否满足 。当条件不满足时,该线程就要在线程等待队列中排队,直到满足条件执行临界区代码。
那有的人就有疑问了,条件变量资源也是在临界区的呀,如果线程获取锁资源之后,再通过条件变量判断是不满足条件,那么该线程就会带着锁资源在等待队列中等待,那其他线程就没有机会持有锁,所以整个进程就被阻塞住了,这不就是占着茅坑不拉屎嘛 ~
因此,为了避免死锁问题,条件变量是具有自动释放锁的能力。这也就是为什么该函数的第二个参数要传互斥锁地址的原因了。
【总结】
- 当调用
pthread_cond_wait
时,首先会通过条件变量来判断该线程是否满足执行临界区代码的条件。如果不满足,线程则会自动释放由mutex
指向的互斥锁,这是pthread_cond_wait
函数内部的一部分操作。- 然后线程将进入线程等待队列中排队并等待条件的变化。(线程等待队列:通常是由操作系统内核管理的)
- 在等待队列中,线程将处于阻塞状态,不会消耗
CPU
时间,直到另一个线程通过pthread_cond_signal
或pthread_cond_broadcast
唤醒它,当调用这两个函数其中一个时,通常是因为线程满足条件了,即线程将重新尝试获取之前释放的互斥锁mutex
,并继续执行临界区代码。
2.5 唤醒线程
条件变量中的线程是需要被唤醒的,否则线程等待队列也不知道何时可以执行临界区的代码了。
唤醒线程有两种方式
pthread_cond_signal
函数:只会唤醒一个正在等待在线程等待队列的线程,通常是最先进入等待队列的线程,也就是队头线程。
c
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast
函数: 用于唤醒所有等待在等待队列上的线程,挨个通知该队列中的所有线程访问临界资源。因此称为广播。
c
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
说明:
- 参数:表示想要从哪个条件变量中唤醒线程。
- 返回值:销毁成功返回
0
,失败返回非0
的错误码。具体的错误码可以用errno
来获取具体的错误信息。 - 即使没有线程在等待队列上,调用以上函数没有任何作用。
2.6 简单使用线程同步相关接口
创建5
个线程,然后再创建一个初始值为0
的全局变量count
,每一个线程都要并发对该资源做++
操作。
要求:5
个线程必须要按顺序执行。
cpp
#include <unistd.h>
#include <iostream>
#include <pthread.h>
using namespace std;
const int NUM = 5; // 线程个数
int count = 0; // 临界资源
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 创建锁资源
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 创建条件变量
void *thread_task(void *args)
{
// 线程分离:让操作系统回收线程的资源
pthread_detach(pthread_self());
// 我的Linux机器默认是64位,指针是8字节,因此我使用8字节的long long
// 而不是用int是因为会发生截断。
long long number = (long long)args; // 线程编号1~5
cout << "pthread" << number << "创建成功..." << endl;
while (true)
{
// 加锁
pthread_mutex_lock(&lock);
// 条件等待
pthread_cond_wait(&cond, &lock);
cout << "pthread" << number << ": count = " << ++count << endl;
// 解锁
pthread_mutex_unlock(&lock);
}
}
int main()
{
for (long long i = 1; i <= NUM; i++)
{
pthread_t tid;
pthread_create(&tid, nullptr, thread_task, (void *)i);
// 通过适当的休眠,可以减少线程之间的竞争条件
// 即减少因多个线程同时被创建而可能导致的竞争和不确定性。
sleep(1);
}
sleep(3); // 让所有线程都去等待队列中等待
cout << "所有线程全部入队列" << endl;
// 主线程负责唤醒线程
while (true)
{
pthread_cond_signal(&cond); // 唤醒等待队列第一个线程
cout << "主线程唤醒了一个线程..." << endl;
sleep(1);
}
return 0;
}
【运行结果】
如上,认真看你会发现线程1~5
运行是井然有序的,像是在排队依次执行一样,这就是同步!
另外,我们可以将唤醒方式换成广播
【程序结果】
在广播之下,仍然有序~
三、代码
本篇博客的相关代码:点击跳转