二、线程同步
1.条件变量
引入
【故事】假设有一个VIP自习室,门口有一把钥匙,每天第一个来的就可以进自习室学习,一次允许一个人在自习室;张三早上5点来,抢到了第一个进入自习室学习的机会,张三拿着门口钥匙打开了自习室的门进入自习室学习,学了3小时后,张三要出去上厕所,张三打开门一看,外面有许多人排队,如果因为上厕所而放弃了这个自习室机会就不好了,于是张三带着钥匙去上厕所,回来了继续学习;到午饭时间了,张三要去吃午饭,准备把钥匙挂在墙上,张三后面又想,等我再次过来的时候,我就要排到最后一个队伍了,正准备将钥匙挂在墙上,后面又拿回去,再进自习室学一会,在自习室里面又饿的不行,又把钥匙放回门口,后面又立马拿回来进自习室反反复复,张三由于饥饿,来回在自习室与门口挣扎,在自习室里也没有干什么,造成了时间浪费,也让后面的人着急
这种事情没有错,但是不合理--->因为不高效
于是自习室有了新规定:每一个自习完成的同学,归还了钥匙之后不能立马申请,要二次申请必须要重新排队,这样 就合理了,这种保证线程顺序性就是同步
条件变量
-
当一个线程互斥的访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了
-
例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
同步概念与竞态条件
-
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
-
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解
2.条件变量接口
在Linux环境下,条件变量(Condition Variables)通常与互斥锁(Mutexes)一起使用,以实现在多线程编程中的同步机制。条件变量可以使得线程在某个条件不满足时挂起(等待),直到某个条件成立时被唤醒。
Linux下条件变量的接口主要定义在POSIX线程库(pthread)中,以下是一些常用的条件变量相关函数:
pthread_cond_init
- 初始化条件变量。
其中,cond
是指向要初始化的条件变量的指针,attr
是条件变量的属性,如果为NULL
,则使用默认属性。
pthread_cond_destroy
- 销毁条件变量。
当不再需要一个条件变量时,应该调用这个函数来释放它所占用的资源。
pthread_cond_wait
- 等待条件变量。
-
cond
:指向一个已经初始化的条件变量。 -
mutex
:指向一个已经初始化并且被当前线程持有的互斥锁。
函数的行为如下:
-
调用
pthread_cond_wait
时,互斥锁mutex
必须由当前线程持有。 -
pthread_cond_wait
会自动释放mutex
,这样其他线程可以获取这个互斥锁。 -
当前线程进入等待状态,直到条件变量
cond
被信号(pthread_cond_signal
或pthread_cond_broadcast
)。 -
当
cond
被信号时,线程从等待状态中被唤醒,并且重新获取mutex
。 -
pthread_cond_wait
返回时,互斥锁mutex
再次被当前线程持有。
pthread_cond_timedwait
- 带超时的等待条件变量。
与pthread_cond_wait
类似,但增加了一个超时时间,如果在指定的时间到达之前条件变量没有被信号唤醒,那么函数返回ETIMEOUT。
pthread_cond_signal
- 唤醒一个等待条件变量的线程。
pthread_cond_broadcast
- 唤醒所有等待条件变量的线程。
唤醒所有等待该条件变量的线程。
在使用这些函数时,应当确保互斥锁的正确使用,以避免死锁或资源竞争。条件变量通常用于生产者-消费者问题等同步场景中。
为什么pthread_cond_wait
需要互斥量
-
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程
-
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
-
按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:
-
由于解锁和等待不是原子操作。调用解锁之后,
pthread_cond_wait
之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_cond_wait
将错过这个信号,可能会导致线程永远阻塞在这个pthread_cond_wait
。所以解锁和等待必须是一个原子操作。 -
int pthread_cond_wait (pthread_cond_t *cond,pthread_mutex_t * mutex)
; 进入该函数后,会去看条件量等于 0 不?等于,就把互斥量变成 1,直到 cond_wait 返回,把条件量改成 1,把互斥量恢复成原样。
3.认识条件变量例子
假设有一个盘子,A同学带上眼罩负责从盘子中拿苹果,B同学不能说话把苹果放在盘子里;如果没有规则,那么会造成A同学不停的拿苹果,无论在与不在,B呢想放就放,不想放就一直不放,但是A确傻傻的一直去拿,浪费时间,占用资源效率低;
为了避免效率低下,B同学拿来一个铃铛,A同学第一次拿没有拿到苹果,A同学就不拿了,B同学把苹果放在盘子里,就敲一下铃铛,告诉A,现在苹果在盘子里了,你可以拿了,于是A收到铃铛信号,A就拿到了苹果,这样就大大提高了效率
这个铃铛就是条件变量
现在C同学也带上眼罩去拿苹果,也就是有两个同学拿苹果,为了提高效率,A和C排队,B敲一下铃铛表明盘子有一个苹果,B敲2下表示盘子有两个苹果,B敲一下A拿到苹果,B等待,A重新排队在B后面;A又敲一下B也拿到苹果,B又排队在A后面;B敲两下,A拿完B拿,又有序排队等待
通过这个例子我们发现,条件变量需要一个线程队列,还需要有一个通知机制(叫醒一个,还是全部叫醒)
-
【测试代码】
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
const int num = 5;
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;
void *Wait(void *args)
{
std::string name = static_cast<const char *>(args);
while (1)
{
pthread_mutex_lock(&gmutex);
pthread_cond_wait(&gcond, &gmutex);
usleep(10000);
std::cout << "I am : " << name << std::endl;
pthread_mutex_unlock(&gmutex);
}
}
int main()
{
pthread_t threads[num];
for (int i = 0; i < num; i++)
{
char *name = new char[1024];
snprintf(name, 1024, "thread-%d", i + 1);
pthread_create(threads + i, nullptr, Wait, (void *)name);
}
sleep(1);
// 唤醒其他线程
while (1)
{
pthread_cond_signal(&gcond);
//pthread_cond_broadcast(&gcond);
std::cout << "唤醒一个线程...." << std::endl;
sleep(2);
}
for (int i = 0; i < num; i++)
{
pthread_join(threads[i], nullptr);
}
return 0;
}
这里就不多说了,其中可以唤醒一个线程,也可以同时唤醒所有线程,并且唤醒顺序是与创建一致的;如果是一次性唤醒全部,唤醒打印输出可能不一致,那是由于我们没有对打印至显示器进行加锁