Linux:线程同步
-
- 线程同步
-
- [条件变量 cond](#条件变量 cond)
线程同步
同步的含义是:
同步:线程之间按照一定顺序访问资源
线程在占用锁的时候,是不受控制的,这就有可能导致一个竞争能力强的线程,从头到尾都占用一个锁。刚释放这个锁,就又被同一个线程申请走了。
线程是通过条件变量
来进行线程同步的。
条件变量 cond
条件变量cond
在pthread
库中,需要头文件<pthread.h>
,创建与销毁方式如下:
条件变量的类型是pthread_cond_t
,分为全局条件变量
和局部条件变量
,它们的创建方式不同。
全局cond:
想要创建一个全局的条件变量很简单,直接定义即可:
cpp
pthread_cond_t xxx = PTHREAD_COND_INITIALIZER;
这样就创建了一个名为xxx
的变量,类型是pthread_cond_t
,即这个变量是一个条件变量
,全局的条件变量必须用宏PTHREAD_COND_INITIALIZER
进行初始化!
另外,全局的条件变量不需要手动销毁。
局部cond:
局部的条件变量是需要通过接口来初始化与销毁的,接口如下:
pthread_cond_init:
pthread_cond_init
函数用于初始化一个条件变量
,函数原型如下:
cpp
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
函数用于销毁一个条件变量
,函数原型如下:
cpp
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
,通过相同的方式进入了等待队列
:
现在所有线程都在等待队列
中,这些线程因为没有满足特定条件,所以不能访问临界资源。但是为什么要进等待队列呢?
因为要保证线程同步
,也就是说,一开始谁先访问的临界资源,那么后续条件满足时,就让谁先来访问这个资源。
pthread_cond_wait:
pthread_cond_wait
函数用于让一个线程进入等待队列
等待,直到被唤醒,函数原型如下:
cpp
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
,失败返回错误码
。
那么进入等待队列后,又要如何唤醒内部的线程呢?
有两种唤醒方式:唤醒一个线程
与唤醒所有线程
pthread_cond_signal:
pthread_cond_signal
用于唤醒等待队列
的第一个线程,让其访问临界区的代码,函数原型如下:
cpp
int pthread_cond_signal(pthread_cond_t *cond);
参数:cond
用于指明一个条件变量,说明要唤醒哪一个条件变量下等待的线程。
比如刚刚三个线程都进入了等待队列
:
当使用pthread_cond_signal
唤醒一个线程时:线程重新获得之前释放的锁mutex
,随后访问临界区代码。
当后续条件再次满足,thread-2
和thread-3
也会依次再次获得锁,从而访问到临时资源。
假设现在thread-1
访问完毕临界资源后,立马再次申请了锁:
由于条件变量的存在,therad-1
不能直接访问资源,要去等待队列
等待,此时thread-1
进入等待队列
尾部:
这样就可以避免一个线程一直占用临界资源
,从而完成线程同步
了。
pthread_cond_broadcast:
pthread_cond_broadcast
用于唤醒等待队列中的所有线程,函数原型如下:
cpp
int pthread_cond_broadcast(pthread_cond_t *cond);
参数:cond
用于指明一个条件变量,说明要唤醒哪一个条件变量下等待的线程。
还是刚才的三个线程都处于等待队列的情况:
当我现在用pthread_cond_broadcast
唤醒所有线程,此时thread-1
,thread-2
,thread-3
都被唤醒了,难道他们一起访问临界资源吗?不是的,此时所有被唤醒的线程再次竞争同一把锁,竞争到锁的线程才访问临界资源。当前一个线程访问完毕后,剩下的线程继续竞争,再访问临界资源。
讲解完条件变量的接口,我写一个示例帮助大家理解。
cpp
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;
}
首先我简单封装了一个线程类:
cpp
class thread
{
public:
pthread_t _tid;
string _name;
};
其包含两个成员,_tid
存储该线程的TID
,_name
存储该线程的名字。
随后定义了两个全局变量,分别是互斥锁g_mutex
以及条件变量g_cond
。
主线程执行函数master
:
cpp
void master(string name)
{
while(true)
{
sleep(1);
pthread_cond_signal(&g_cond);
}
}
也就是每秒钟通过ptherad_cond_signal
唤醒一个队列中的线程。
在main
函数中:
cpp
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
函数如下:
cpp
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
改为如下代码:
cpp
void master(string name)
{
while(true)
{
sleep(3);
cout << "-------------" << endl;
pthread_cond_broadcast(&g_cond);
}
}
现在主线程每隔三秒唤醒等待队列
中的所有线程,为了方便观察,我每次额外输出一条横线-----------
。
输出结果:
由于线程被全部唤醒后,此时它们又要再次竞争锁,每个轮次情况不一样,所以输出的顺序就不一样。比如第一次输出,thread-1
竞争力比较强,而第三次输出,thread-2
竞争力比较强。但是可以保证的是,每个轮次中1 2 3 4 5
都获取到了一次资源,因此也算一种同步。