个人主页 : 个人主页
个人专栏 : 《数据结构》 《C语言》《C++》《Linux》
文章目录
- 前言
- 线程同步
- 条件变量接口
- 简单示例
- pthread_cond_wait为什么要有mutex
- 伪唤醒问题的解决 (if->while)
- 总结
前言
本文作为我对于线程同步知识总结
线程同步
- 同步:在保证数据安全的前提下,让线程能够按照某种顺序访问临界资源,从而有效避免饥饿问题,叫做同步
- 竞态条件:是指多个线程同时访问系统共享资源时,由于其执行顺序或时间上的不确定性,导致数据不一致或其它不可预料的结果(一般发生在对称多处理环境,中断和异常处理,内核态抢占,并发执行…)
看了上面两个概念,你可能还是不太理解同步是什么,为什么要有同步。下面我们就举一个例子。
我们假定有两个人,一个人A将苹果放在桌子上,另一个人B蒙着眼睛去桌子上拿苹果,桌子每次只允许有一个人,因为B不清楚桌子上的情况是什么(有一个苹果,没有苹果,桌子上全是苹果),那为了保险起见,B只能疯狂的去桌子上拿苹果,以保证B拿到所有的苹果。那如果桌子上没有苹果,B仍然疯狂去桌子上拿苹果,此时A是不是就不能去桌子上放苹果,那此时B是不是再做无用工,而且导致了A不能放苹果,B拿苹果的效率降低。如果我们将A,B换成线程,苹果看出共享资源(某种任务),桌子看成临界区,那B是不是就是一直在做申请锁,再释放锁的无效工作,并且导致了A的饥饿问题。这时我们就需要保证A,B之间的顺序问题,如在B访问过桌子后,不能立即再次访问桌子,要等待A访问桌子后。这是不是就会使A,B拿苹果的效率提升。而这就是为什么要有同步的理由
条件变量接口
初始化条件变量
- 静态初始化
与互斥锁类似,定义一个全局的条件变量,用PTHREAD_COND_INITIALIZER宏来初始化,系统自动释放该条件变量。该宏一般存放在 /usr/include/pthread.h 路径下
- 动态初始化
restrict是C语言的一个关键字,表示该指针是唯一的访问其指向对象的指针。
cond将被初始化的条件变量,attr 为nullptr,使用默认属性初始化条件变量。
如果函数成功执行,返回0; 如果函数执行失败,则返回错误码,如EAGAIN(资源暂时不可用),ENOMEM(内存不足)
销毁条件变量
cond 表示将被销毁的条件变量,需要注意在调用pthread_cond_destroy后,该指针本身并未被销毁,只是所指向的条件变量被销毁,记得将指针置空
如果函数成功执行,返回0; 如果函数失败,返回错误码。如EBUSY,该条件变量正在被使用
等待条件变量
cond 表示将要等待的条件变量,mutex 表示线程所持有的互斥锁
如果函数成功执行,返回0;如果函数执行失败,返回错误码,如EINVAL 无效的参数(条件变量 or 互斥锁为初始化…)
关于该函数的参数为什么会有锁,有什么注意事项,在下面代码示例,我们再解释。
唤醒等待
cond要发生信号的条件变量指针。
如果函数执行成功,返回0; 如果函数执行失败,返回错误码,如ENVAL(无效的参数)
需要注意的是,pthread_cond_signal只用于唤醒在cond条件变量的阻塞队列中等待的一个线程。
该函数的参数与返回值与pthread_cond_signal相同,只不过该函数唤醒所有在cond条件变量的阻塞队列中等待的所有线程,而那些线程先执行,取决于操作系统的调度策略。
简单示例
我们先来看看下面代码,3个线程争夺ticket资源。当三个线程检测到ticket == 0时,三个线程都将等待。我们主线程每过5秒,使ticket += 10。
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>using namespace std;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ticket = 100;void* threadRoutine(void *args)
{const string threadname = static_cast<char*>(args);while(true){ usleep(1000);pthread_mutex_lock(&mutex);if(ticket > 0){ticket--;cout << threadname << ", get a ticket: " << ticket << endl;}else{cout << threadname << ", ticket == 0" << endl;pthread_cond_wait(&cond, &mutex);}pthread_mutex_unlock(&mutex);}return nullptr;
}int main()
{pthread_t td1;pthread_create(&td1, nullptr, threadRoutine, (void*)"thread-1");pthread_t td2;pthread_create(&td2, nullptr, threadRoutine, (void*)"thread-2");pthread_t td3;pthread_create(&td3, nullptr, threadRoutine, (void*)"thread-3");while(true){sleep(5);pthread_mutex_lock(&mutex);ticket += 10;pthread_mutex_unlock(&mutex);pthread_cond_signal(&cond);}pthread_join(td1, nullptr);pthread_join(td2, nullptr);pthread_join(td3, nullptr);return 0;
}
当线程2,线程3,线程1先后检测到ticket == 0时,在cond的等待队列中,线程以2,3,1的顺序排队。那当调用pthread_cond_signal函数时,线程2会先执行,执行完再检测到ticket==0,排到等待队列尾部
再调用pthread_cond_signal函数时,线程3会执行。
再调用pthread_cond_signal函数时,线程1会执行。
这样,我们多线程就可以按某种特定顺序来执行。
那如果我们使用pthread_cond_broadcast函数,会有什么情况?
我们会发现,三个线程都被唤醒,来争抢ticket资源,其先后顺序由操作系统决定(优先级,竞争锁的能力…)。
pthread_cond_wait为什么要有mutex
此时不知道你是否有一个疑问,我们假定线程-1先争抢到ticket,检测到ticket == 0时,该线程-1要在cond的等待队列中等待,然后其它两个线程在进入临界区,检测到ticket==0,在等待队列中排队。但是线程-1是持有锁进入等待队列的,那其它两个线程是如何进入临界区的?答案很明显,那就是线程-1在cond等待中,一定释放了其持有的锁,从而使其余两个线程可以持有锁进入临界区。这就是为什么pthread_cond_wait函数的参数要有互斥锁的存在。
那我们在深入想一想,调用pthread_cond_wait函数后,该线程是先释放锁,再进入等待队列中;还是先进入等待队列中,再释放锁?答案是都不是。释放锁和进入等待队列是同时进行的!!!这也表示pthread_cond_wait函数是原子的,在调用pthread_cond_wait时,涉及的互斥锁释放和进入等待队列的操作是作为一个不可分割的整体来执行。确保了线程在调用该函数时不会遇到竞态条件,即线程在释放锁和进入等待队列之间不会被其它线程打断。
以下是线程调用pthread_cond_wait的过程
-
线程必须已经持有锁:在调用pthread_cond_wait时,线程必须已经锁定了某个互斥锁,这是该函数的前提条件
-
自动释放互斥锁:当线程调用pthread_cond_wait时,它会自动释放它当前持有的互斥锁。这一步是为了允许其它线程有机会获取该互斥锁并修改共享资源,从而可能改变条件变量的状态
-
加入等待队列:释放互斥锁之后,线程会接着被添加到条件变量的等待队列中,并在此处等待
-
等待被唤醒:线程在等待队列中等待,直到其它线程调用pthread_cond_signal 或 pthread_cond_broadcast来唤醒它
-
重新获取互斥锁:当线程被唤醒时,线程会尝试重新获取之前释放的互斥锁。如果锁此时没有被其它线程持有,线程将成功获取锁并继续执行。如果锁仍然被其它线程持有,线程将阻塞(在申请锁的地方阻塞),直到能够获取锁时。
具体来说,pthread_cond_wait函数的内部实现保证了2,3步骤的原子性。
伪唤醒问题的解决 (if->while)
伪唤醒问题是指:在多线程环境中,当线程等待某个条件变量时,它可能会在没有到达预期的情况下被唤醒。
对于这一问题,我们可以在判断的时候,将if 变为 while,使其被唤醒后任然进行条件判断,如果条件满足,线程继续向后执行,如果条件不被满足,线程继续在该条件变量下等待。
pthread_mutex_lock(&mutex);
// 访问临界区
while(条件为假)pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
总结
以上就是我对于线程同步的总结。