👉🏻 完成两个线程通过条件变量实现交替打印
1.题目描述:线程A打印-我是线程A;线程B打印-我是线程B; 最终实现交替打印,不能出现连续的相同打印。
2.本题主要考察条件变量的基本使用流程
错误代码加优化(c++线程库版本)
#include<iostream>
#include<mutex>
#include<thread>
#include<condition_variable>
#include<unistd.h>
using namespace std;
mutex mx;//互斥锁
condition_variable cv;//条件变量
bool flag = true;
void Print(char args)
{
char c = static_cast<char>(args);
for (int i = 0; i < 5; i++)
{
unique_lock<mutex> lock(mx); // 上锁
//if(!flag)
cv.wait(lock,[]{return flag;}); // 等待条件 flag 为 true[]{return flag||!flag;}
if(c=='A')
cout << "我是线程A" << endl;
else if(c=='B')
cout<<"我是线程B"<<endl;
flag = !flag;
cv.notify_one(); // 通知另一个线程
sleep(1);
}
}
int main()
{
//创建AB线程
std::thread threadA(Print,'A');
std::thread threadB(Print,'B');
// 等待线程结束
threadA.join();
threadB.join();
return 0;
}
此代码会导致第一次打印字符A后就阻塞不动了,
主要原因出在这行代码上
cv.wait(lock,[]{return flag;});
wait的第二个参数是个函数对象(这里是lamda表达式),当返回值是false时,wait会进行阻塞,true时,wait会开始释放锁,当前线程拿到锁后开始继续执行。
因为flag一开始为true,所以此时线程A是拿到锁并执行打印工作的。
而后将flag改为false,并去唤醒线程B(唤醒它来看看wait是否满足情况了),此时第二次循环进来,线程A会被wait阻塞住。
而线程B这边已经苏醒,过来第一次循环,结果被阻塞,这是怎么回事?因为flag被改为false了,所以线程B阻塞了,而此时线程A同时也被阻塞,直接导致两个线程统统被阻塞,所以这个wait的判定条件是个大问题。
要解决的话就是将线程A和线程B进来的不同情况都要考虑进去。
代码优化如下:
#include<iostream>
#include<mutex>
#include<thread>
#include<condition_variable>
#include<unistd.h>
using namespace std;
mutex mx;//互斥锁
condition_variable cv;//条件变量
bool flag = true;
void Print(char args)
{
char c = static_cast<char>(args);
for (int i = 0; i < 5; i++)
{
unique_lock<mutex> lock(mx); // 上锁
cv.wait(lock,[&]{return c=='A'&&flag||c=='B'&&!flag;}); // 等待条件 flag 为 true[]{return flag||!flag;}
cout<<"我是线程"<<c<<endl;
flag = !flag;
cv.notify_one(); // 通知另一个线程
sleep(1);
}
}
int main()
{
//创建AB线程
std::thread threadA(Print,'A');
std::thread threadB(Print,'B');
// 等待线程结束
threadA.join();
threadB.join();
return 0;
}
版本2(使用phtread.h库)
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
//声明互斥锁和条件变量
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
bool flag = false;
void* Print(void* args)
{
char c = *(static_cast<char*>(args));
for(int i = 0;i<5;i++)
{
//先上锁为敬
pthread_mutex_lock(&mtx);
//判断条件变量
while(c=='A'&&flag||c=='B'&&!flag)
{
pthread_cond_wait(&cv,&mtx);//线程阻塞,等待唤醒
}
cout<<"我是线程"<<c<<endl;
flag = !flag;
sleep(1);
pthread_cond_signal(&cv);//唤醒其它线程
pthread_mutex_unlock(&mtx);//解锁,当其它线程被唤醒时能拿到锁
}
return nullptr;
}
int main()
{
//1.线程声明
pthread_t threadA,threadB;
char c1 = 'A',c2 = 'B';
//2.创建线程
pthread_create(&threadA,nullptr,Print,(void*)&c1);
pthread_create(&threadB,nullptr,Print,(void*)&c2);
//3.线程等待回收
pthread_join(threadA,nullptr);
pthread_join(threadB,nullptr);
//4.互斥锁和条件变量销毁
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cv);
return 0;
}
这里有几个需要注意的要点:
1.pthread_create
中的线程函数返回值必须是void类型的,参数为void,也就是指针类型
2.使用 while 循环则可以避免虚假唤醒的问题。当线程被唤醒时,它会重新检查条件是否满足。如果条件不满足,线程将继续等待,直到条件满足为止。这样可以确保线程在接收到正确的信号时才会继续执行。
👉🏻按序打印
原题链接:按序打印
mycode:
class Foo {
mutex mtx;
condition_variable cv;
int num = 0;
public:
Foo() {
}
void first(function<void()> printFirst) {
// printFirst() outputs "first". Do not change or remove this line.
unique_lock<mutex> lock(mtx);
cv.wait(lock,[&]{return num==0;});
printFirst();
num = (num+1)%3;
cv.notify_all();//唤醒全部线程
}
void second(function<void()> printSecond) {
// printSecond() outputs "second". Do not change or remove this line.
//上锁
unique_lock<mutex> lock(mtx);
cv.wait(lock,[&]{return num==1;});
printSecond();
num = (num+1)%3;
cv.notify_one();//唤醒一个线程
}
void third(function<void()> printThird) {
// printThird() outputs "third". Do not change or remove this line.
unique_lock<mutex> lock(mtx);
cv.wait(lock,[&]{return num==2;});
printThird();
num = (num+1)%3;
//cv.notify_one();//唤醒一个线程
}
};
这里要注意的是唤醒线程只需要一个notify_all和notify_one就行了,这样就只会执行3次操作而已,不会过多导致时间超时
为什么呢?因为不管哪个函数先进入,如果因为条件不满足,会堵塞在那里,等待被唤醒,一个notify_all先将其余两个线程唤醒,此时这两个线程过来查看条件变量情况,一个会成功,一个会失败阻塞,这个时候还差一个线程还没执行完全在等待,就再来一个notify_one唤醒即可。
👉🏻 H2O 生成
原题链接:H2O 生成
mycode:
int num_h = 0;
class H2O {
mutex mtx;
condition_variable cv;
public:
H2O() {
}
void hydrogen(function<void()> releaseHydrogen) {
// releaseHydrogen() outputs "H". Do not change or remove this line.
unique_lock<mutex> lock(mtx);
cv.wait(lock,[&]{return num_h<2;});//H的数量要小于2
releaseHydrogen();
++num_h;
cv.notify_all();//唤醒氧线程
}
void oxygen(function<void()> releaseOxygen) {
// releaseOxygen() outputs "O". Do not change or remove this line.
unique_lock<mutex> lock(mtx);
cv.wait(lock,[&]{return num_h==2;});
num_h = 0;//H与O匹配消耗完后清空
releaseOxygen();
cv.notify_all();//唤醒氢气线程
}
};
👉🏻生产者消费者模型实现
版本1
#include<iostream>
#include<vector>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<unistd.h>
using namespace std;
const size_t BUFFER_SZ = 10;
vector<int> buffer;
mutex mtx;
condition_variable cv;
void Consumer(void* args)
{
const char* name = static_cast<const char*>(args);
for(int i = 0;i<BUFFER_SZ;i++)
{
//1.上锁
unique_lock<mutex> lock(mtx);
//2.条件变量
cv.wait(lock,[]{return buffer.size()>0;});//缓冲区得有数据才能消费
int data = buffer.back();
printf("%s取走了数据%d,此时缓冲区数据数量有:%d\n",name,data,buffer.size());
buffer.pop_back();
sleep(2);//我们让消费者消费慢一点
lock.unlock();
cv.notify_one();//唤醒生产者线程
}
}
void Productor(void* args)
{
const char* name = static_cast<const char*>(args);
for(int i = 0;i<BUFFER_SZ;i++)
{
//1.上锁
unique_lock<mutex> lock(mtx);
//2.条件变量
cv.wait(lock,[]{return buffer.size()<=BUFFER_SZ;});//缓冲区得有数据才能消费
buffer.push_back(i);
printf("%s生产了数据%d,此时缓冲区数据数量有:%d\n",name,i,buffer.size());
sleep(1);
lock.unlock();
cv.notify_one();//唤醒消费者线程,有数据给你消费了快来
}
}
int main()
{
thread consumer(Consumer,(void*)"Consumer");
thread productor(Productor,(void*)"Productor");
//线程回收
consumer.join();
productor.join();
return 0;
}
效果如下:
这里和我理想的好像不一样,这里是全部生产完再消费,并没有实现生产和消费同步
更改之后:
#include<iostream>
#include<vector>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<unistd.h>
using namespace std;
const size_t BUFFER_SZ = 10;
vector<int> buffer;
mutex mtx;
condition_variable cv_consumer;
condition_variable cv_productor;
void Consumer(void* args)
{
const char* name = static_cast<const char*>(args);
for(int i = 0;i<BUFFER_SZ;i++)
{
//1.上锁
unique_lock<mutex> lock(mtx);
//2.条件变量
cv_consumer.wait(lock,[]{return buffer.size()>0;});//缓冲区得有数据才能消费
int data = buffer.back();
printf("%s取走了数据%d,此时缓冲区数据数量有:%d\n",name,data,buffer.size());
buffer.pop_back();
lock.unlock();
cv_productor.notify_one();//唤醒生产者线程
sleep(2);//我们让消费者消费慢一点
}
}
void Productor(void* args)
{
const char* name = static_cast<const char*>(args);
for(int i = 0;i<BUFFER_SZ;i++)
{
//1.上锁
unique_lock<mutex> lock(mtx);
//2.条件变量
cv_productor.wait(lock,[]{return buffer.size()<=BUFFER_SZ;});//缓冲区得有数据才能消费
buffer.push_back(i);
printf("%s生产了数据%d,此时缓冲区数据数量有:%d\n",name,i,buffer.size());
lock.unlock();
cv_consumer.notify_one();//唤醒消费者线程,有数据给你消费了快来
sleep(1);
}
}
int main()
{
thread consumer(Consumer,(void*)"Consumer");
thread productor(Productor,(void*)"Productor");
//线程回收
consumer.join();
productor.join();
return 0;
}
主要更改点:
lock.unlock();
cv_productor.notify_one();//唤醒生产者线程
sleep(2);//我们让消费者消费慢一点
lock.unlock();
cv_consumer.notify_one();//唤醒消费者线程,有数据给你消费了快来
sleep(1);
1.在notify_one前unlock:这里因为我们要实现的就是同步,注意,同步!,如果不手动unlock会导致,比如这里是我将消费者唤醒了,但是如果生产者notify后还有做事情(sleep),互斥锁的生命周期还没结束也就是还没自动释放互斥锁,此时消费者被唤醒了也只能在那尴尬着什么都干不了。结果呢,当生产者终于进入下个循环时,当互斥锁自动释放后,在新的循环里,生产者又加锁了,可谓无缝衔接,所以,根本没有机会留给消费者
所以对症下药,我们可以这么做:
- 可以不unlock,但是notify后别再sleep做其它耗时的事情了:但是这样子速度就太快了,在观察的时候不好看
- 所以还是nitify前unlock才行
2.sleep放在unlock后面:当锁释放后,
版本2
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <condition_variable>
#include<unistd.h>
const int BUFFER_SIZE = 10;
std::vector<int> buffer;
std::mutex mtx;
std::condition_variable bufferNotEmpty;
std::condition_variable bufferNotFull;
std::condition_variable cv;
void producer() {
for (int i = 0; i < 20; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return buffer.size() < BUFFER_SIZE; });
buffer.push_back(i);
std::cout << "Produced: " << i << std::endl;
lock.unlock();
cv.notify_one();
//std::this_thread::sleep_for(std::chrono::milliseconds(500));
sleep(1);
}
}
void consumer() {
for (int i = 0; i < 20; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !buffer.empty(); });
int item = buffer.back();
buffer.pop_back();
std::cout << "Consumed: " << item << std::endl;
lock.unlock();
cv.notify_one();
//std::this_thread::sleep_for(std::chrono::milliseconds(700));
sleep(2);
}
}
int main() {
std::thread producerThread(producer);
std::thread consumerThread(consumer);
producerThread.join();
consumerThread.join();
return 0;
}
这里弄了两个条件变量主要是为了区分唤醒生产者线程和消费者线程,因为这里只有一个生产者线程和一个消费者线程所以其实定义一个条件变量也就够了。
但是为了规范点,如果以后遇到多个生产者线程和多个消费者线程,就必须用对应的条件变量去阻塞且唤醒相关类型的线程!
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长