【Linux】--- 线程同步
一、同步的概念
同步:线程之间按照一定顺序访问资源
线程在占用锁的时候,是不受控制的,这就有可能导致一个竞争能力强的线程,从头到尾都占用一个锁。刚释放这个锁,就又被同一个线程申请走了。
线程是通过条件变量来进行线程同步的。
二、条件变量cond
条件变量函数
条件变量cond在pthread库中,需要头文件<pthread.h>,创建与销毁方式如下:
条件变量的类型是pthread_cond_t,分为全局条件变量和局部条件变量,它们的创建方式不同。
(1)全局cond:
想要创建一个全局的条件变量很简单,直接定义即可:
pthread_cond_t xxx = PTHREAD_COND_INITIALIZER;
这样就创建了一个名为xxx的变量,类型是pthread_cond_t,即这个变量是一个条件变量,全局的条件变量必须用宏PTHREAD_COND_INITIALIZER进行初始化!
另外,全局的条件变量不需要手动销毁。
(2)局部cond:
局部的条件变量是需要通过接口来初始化与销毁的,接口如下:
pthread_cond_init:
pthread_cond_init函数用于初始化一个条件变量,函数原型如下:
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参数:
- restrict cond:类型为pthread_cond_t *的指针,指向一个条件变量,对其初始化
- restrict attr:用于设定该条件变量的属性,一般不用,设为空指针即可
返回值:成功返回0;失败返回错误码
pthread_cond_destroy:
pthread_cond_destroy函数用于销毁一个条件变量,函数原型如下:
int pthread_cond_destroy(pthread_cond_t *cond);
参数:类型为pthread_cond_t *的指针,指向一个条件变量,销毁该条件变量
返回值:成功返回0;失败返回错误码
创建好条件变量后,就要使用这个条件变量,主要是两个操作:等待条件满足和唤醒线程。
如图所示:
现在有三个线程thread-1,thread-2,thread-3,这三个线程争夺一个临界资源。
而thread-1申请到了锁mutex,但是由于我们给这个临界资源添加了条件变量:此时thread-1不能直接访问临界资源,而是进入等待队列,并且释放持有的锁:
由于锁被释放,后续线程可以继续申请这个锁。
于是thread-2和thread-3也分别申请到了mutex,通过相同的方式进入了等待队列:
现在所有线程都在等待队列中,这些线程因为没有满足特定条件,所以不能访问临界资源。
但是为什么要进等待队列呢?
因为要保证线程同步,也就是说,一开始谁先访问的临界资源,那么后续条件满足时,就让谁先来访问这个资源。
(1)pthread_cond_wait:
pthread_cond_wait函数用于让一个线程进入等待队列等待,直到被唤醒,函数原型如下:
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数:
- restrict cond:类型是pthread_cond_t *的指针,即在哪一个条件变量下等待
- restrict mutex:类型是pthread_mutex_t *的指针,因为在进入等待队列前,线程是持有锁的状态,此处传入锁的指针,就是为了帮助这个线程释放该锁,从而让其他线程也可以申请锁,进入等待队列。
返回值:成功返回0,失败返回错误码。
那么进入等待队列后,又要如何唤醒内部的线程呢?
有两种唤醒方式:唤醒一个线程与唤醒所有线程
(2)pthread_cond_signal:
pthread_cond_signal用于唤醒等待队列的第一个线程,让其访问临界区的代码,函数原型如下:
int pthread_cond_signal(pthread_cond_t *cond);
参数:cond用于指明一个条件变量,说明要唤醒哪一个条件变量下等待的线程。
比如刚刚三个线程都进入了等待队列:
当使用pthread_cond_signal唤醒一个线程时:线程重新获得之前释放的锁mutex,随后访问临界区代码。
当后续条件再次满足,thread-2和thread-3也会依次再次获得锁,从而访问到临时资源。
假设现在thread-1访问完毕临界资源后,立马再次申请了锁:
由于条件变量的存在,therad-1不能直接访问资源,要去等待队列等待,此时thread-1进入等待队列尾部:
这样就可以避免一个线程一直占用临界资源,从而完成线程同步了。
(3)pthread_cond_broadcast:
pthread_cond_broadcast用于唤醒等待队列中的所有线程,函数原型如下:
int pthread_cond_broadcast(pthread_cond_t *cond);
参数:cond用于指明一个条件变量,说明要唤醒哪一个条件变量下等待的线程。
还是刚才的三个线程都处于等待队列的情况:
当我现在用pthread_cond_broadcast唤醒所有线程,此时thread-1,thread-2,thread-3都被唤醒了,难道他们一起访问临界资源吗?不是的,此时所有被唤醒的线程再次竞争同一把锁,竞争到锁的线程才访问临界资源。当前一个线程访问完毕后,剩下的线程继续竞争,再访问临界资源。
讲解完条件变量的接口,我写一个示例帮助大家理解。
class thread
{
public:
pthread_t _tid;
string _name;
};
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;
void master(string name)
{
while(true)
{
sleep(1);
pthread_cond_signal(&g_cond);
}
}
void* server(void* argv)
{
thread* thd = (thread*)argv;
while(true)
{
pthread_mutex_lock(&g_mutex);
pthread_cond_wait(&g_cond, &g_mutex);
cout << thd->_name << " is weakup!!!" << endl;
pthread_mutex_unlock(&g_mutex);
}
return nullptr;
}
int main()
{
vector<thread> threads(5);
for(int i = 0; i < 5; i++)
{
threads[i]._name = "thread-" + to_string(i + 1);
pthread_create(&threads[i]._tid, nullptr, server, (void*)&threads[i]);
}
master("master thread");
return 0;
}
首先我简单封装了一个线程类:
class thread
{
public:
pthread_t _tid;
string _name;
};
其包含两个成员,_tid存储该线程的TID,_name存储该线程的名字。
随后定义了两个全局变量,分别是互斥锁g_mutex以及条件变量g_cond。
主线程执行函数master:
void master(string name)
{
while(true)
{
sleep(1);
pthread_cond_signal(&g_cond);
}
}
也就是每秒钟通过ptherad_cond_signal唤醒一个队列中的线程。
在main函数中:
int main()
{
vector<thread> threads(5);
for(int i = 0; i < 5; i++)
{
threads[i]._name = "thread-" + to_string(i + 1);
pthread_create(&threads[i]._tid, nullptr, server, (void*)&threads[i]);
}
master("master thread");
return 0;
}
先创建了五个线程对象,随后在for循环内部通过pthread_create创建线程,以及给这些线程命名,这些线程都去执行了server函数,而主线程执行master函数。
server函数如下:
void* server(void* argv)
{
thread* thd = (thread*)argv;
while(true)
{
pthread_mutex_lock(&g_mutex);
pthread_cond_wait(&g_cond, &g_mutex);
cout << thd->_name << " is weakup!!!" << endl;
pthread_mutex_unlock(&g_mutex);
}
return nullptr;
}
这也是示例中最核心的部分,线程进入了while循环后,先通过pthread_mutex_lock加锁,意图访问临界资源。
但是由于条件变量的限制,其还没有访问到临界资源,就执行了pthread_cond_wait,进入等待队列,并释放掉了自己之前申请的锁。
每秒钟主线程在master中唤醒一个线程,当线程被唤醒后,便重新拿到锁,执行临界资源cout << thd->_name << " is weakup!!!" << endl;,也就是显示器资源,最后释放自己的锁。
注意:在server中,是没有任何sleep函数的,但是由于master中限制了每秒钟唤醒一次,所以最后线程会以同步的形式,每隔一秒依次占用显示器,而不会发生某个线程一直输出。
输出结果:
可以看到,线程按照1 2 3 4 5的顺序依次输出了,这就是线程同步的作用。
我们再看看pthread_cond_broadcast的效果,现在把master改为如下代码:
void master(string name)
{
while(true)
{
sleep(3);
cout << "-------------" << endl;
pthread_cond_broadcast(&g_cond);
}
}
现在主线程每隔三秒唤醒等待队列中的所有线程,为了方便观察,我每次额外输出一条横线-----------。
输出结果:
由于线程被全部唤醒后,此时它们又要再次竞争锁,每个轮次情况不一样,所以输出的顺序就不一样。比如第一次输出,thread-1竞争力比较强,而第三次输出,thread-2竞争力比较强。但是可以保证的是,每个轮次中1 2 3 4 5都获取到了一次资源,因此也算一种同步。