多线程学习(哲学家进餐,生产者消费者模式)

多线程学习(哲学家进餐,生产者消费者模式)

在C++11的多线程编程中,我们首先来看一个最简单的多线程的模型:

#include <iostream> #include <thread>  void printHello() { 	std::cout << "hello world!" << "this is child thread!"<<std::endl; }  int main() { 	std::thread t(printHello); 	//std::thread t([](){printHello();}); 	std::cout << "this is main thread!" << std::endl; 	if (t.joinable()) { 		t.join(); 	} 	return 0; } 

此时,我们有两个线程来完成工作,一个主线程主要负责输出“this is main thread!",另一个子线程负责输出"hello world!this is child thread!"。我们可以运行以下,看看输出结果:
多线程学习(哲学家进餐,生产者消费者模式)
这个就是多线程下的hello world!
接下来,为了体现多线程的高并发,我们以一个简单的例子来表现,有下面一段程序:

#include <iostream> #include <thread> #include <vector> #include <cstdlib> #include <atomic>  class Counter { public: 	void addCount() { m_count++; } 	Counter() : m_count(0) {} 	~Counter() {} 	int count() const { return m_count; } private: 	int m_count; };  void realwork(Counter& c, int times) { 	for (int i = 0; i != times; ++i) { 		c.addCount(); 	} }  int main() { 	Counter c; 	int times = 1000000; 	for (int i = 0; i < times; i++) { 		c.addCount(); 	} 	std::cout << "this is singal thread!  count = " << c.count() << std::endl;  	Counter thread_c; 	std::thread t([&thread_c,times] { 		realwork(thread_c,times/2); 		}); 	realwork(thread_c,times/2); 	if (t.joinable()) { 		t.join(); 	} 	std::cout << "this is multipy thread!   thread_count = " << thread_c.count() << std::endl; 	return 0; } 

我们定义了一个Counter类,专门用来计数,然后在主线程中,计数100万次,然后在子线程中分别计数50万次,运行结果如下:
多线程学习(哲学家进餐,生产者消费者模式)
我们可以看到,在单线程中,确实计数了100万次,而在多线程中只计数了85万次,问题的原因其实很简单,就是在于我们的m_count执行++操作时,主要是,首先从寄存器中读取m_count,再将m_count+1,最后将值重新写入寄存器。然而在并发程序下,我们可能还没有来得及写入寄存器,另一个线程就写入了,导致我们写入的值会减少,这就是为什么我们多线程下的计数次数小于单线程下。
也就是说,对于全局变量,或者说线程中共有的资源,我们在多线程下要将他进行单独访问!防止我们在访问的时候,别的线程对该值进行了修改。
那么我们应该如何来实现这一点呢?
简单来说,有两种方法!
1. 在C++11中提供了原子操作,在atomic库中,我们只需要将Counter类中m_count改成原子,见如下代码:

class Counter { public: 	void addCount() { m_count++; } 	Counter() : m_count(0) {} 	~Counter() {} 	int count() const { return m_count; } private: 	std::atomic<int> m_count; }; 

我们改变了私有成员m_count,这样运行的结果就不会有问题。原子操作就完美地实现了线程的单独操作!但是这种原子操作,我们只能对单一变量进行单独访问,不太方便,接下来引入锁的概念。
2.使用互斥锁mutex
互斥锁mutex存在头文件mutex中,也就是说,我们可以在将要进行的线程操作锁住,不让别的线程进行访问,知道我们当前线程释放锁,别的线程才可以访问,代码实现如下(主函数main不变):

class Counter { public: 	 	void addCount() { m.lock(); m_count++; m.unlock();} 	Counter() : m_count(0) {} 	~Counter() {} 	int count() const { return m_count; } private: 	int m_count; 	std::mutex m; };  void realwork(Counter& c, int times) { 	for (int i = 0; i != times; ++i) { 		c.addCount(); 	} } 

注意到,我们在m_count前后加了锁和释放锁,这样就实现了对m_count的单独操作,以上两种方法运行结果如下:
多线程学习(哲学家进餐,生产者消费者模式)
特别注意的是,我们在使用锁的时候,一定要成对的使用,如果只是单纯的锁住,其他的线程长时间得不到资源,就会导致线程饿死。事实上,mutex的操作时很难的,我们在实际开发中很容易出错,一是锁的没有成对调用,二是锁所锁住的位置不对。
为了确保锁的成对调用,我们可以联想到类的默认构造函数和析构函数,我们清楚,如果一个类被构造出来,那么在程序结束前就一定会调用析构函数,如果我们的代码写的没问题的话,就不会出错,所以以上代码可以这么修改:

template <typename T> class Lock { 	T& m_mutex; public: 	Lock(T& mutex) : m_mutex(mutex) { m_mutex.lock(); } 	~Lock() { m_mutex.unlock(); } };  class Counter { public: 	void addCount() { Lock<std::mutex> lock(m); m_count++; } 	Counter() : m_count(0) {} 	~Counter() {} 	int count() const { return m_count; } private: 	int m_count; 	std::mutex m; }; 

我们可以看到,我们新加入了一个模板类,在默认构造函数中,进行了锁住,在析构函数中释放锁,这样就可以较好地实现锁的成对调用。但是事实上,在C++11中,以及由封装好了锁类供我们使用lock_guard,当然C++11给我们提供的肯定更好,但是它的功能可以理解成我上面的Lock类
具体代码实现如下:

class Counter { public: 	void addCount() { std::lock_guard<std::mutex> lock(m); m_count++; } 	Counter() : m_count(0) {} 	~Counter() {} 	int count() const { return m_count; } private: 	int m_count; 	std::mutex m; };  

这样实现出来的多线程就比较好,保证了锁的成对使用,也给别的人在调用的时候随意更改我们锁的位置,他们只需要调用相应的接口就可以了。

上面比较全面地讲述了互斥锁的使用,接下来我们来讲解读写锁的使用!
比如有两个银行账户,一个是Tom,一个是Jerry,我们在多线程下实现Tom向Jerry的账户里赚钱!我们该如何实现呢?那我们不是只需要把Tom和Jerry的账户分别锁住操作不就可以了吗?
先来看看错误的代码:

#include <iostream> #include <thread> #include <vector> #include <mutex>  class BankAccount { public: 	int Account; 	std::mutex m_mutex; 	BankAccount(int account) : Account(account) {} };  void transferMoney(BankAccount& a, BankAccount& b, int money) { 	//自己向自己的账户赚钱 	if (&a == &b) return; 	std::lock_guard<std::mutex> lockA(a.m_mutex); 	std::lock_guard<std::mutex> lockB(b.m_mutex); 	//自己的账户钱不够 	if (a.Account < money) return; 	a.Account -= money; 	b.Account += money; }  int main() { 	int money = 10; 	BankAccount Tom(100); 	BankAccount Jerry(100); 	std::thread t1([&Tom, &Jerry, money] { 		transferMoney(Tom, Jerry, money); 	});  	std::thread t2([&Tom, &Jerry, money] { 		transferMoney(Jerry, Tom, money+30); 	}); 	if (t1.joinable()) { 		t1.join(); 	} 	if (t2.joinable()) { 		t2.join(); 	} 	std::cout << "Tom has " << Tom.Account << std::endl; 	std::cout << "Jerry has " << Jerry.Account << std::endl; 	return 0; } 

我们可以看到首先Tom向Jerry转了10块钱,然后Jerry向Tom转了40块钱,按理说,Jerry和Tom的账户余额分别是70和130,但是在某一种情况下运行结果会是这样的!
多线程学习(哲学家进餐,生产者消费者模式)
程序一直卡在这地方,这是为什么呢?
我们可以这么想,我们有两个线程来实现转账,在高并发的情况下,线程一和线程二同时进行,首先Tom向Jerry转,把Tom的账户锁住了,此时还没来得及锁住Jerry的账户,第二个线程就把Jerry的账户锁住了,此刻线程一需要线程二的钥匙,而线程二需要线程一的钥匙,所以就发生的死锁现状。
那么我们应该如何来避免这种情况呢?我们只需要这么做,也就是说,账户要么谁都不锁,要么两个都一起锁住。

void transferMoney(BankAccount& a, BankAccount& b, int money, std::mutex& m) { 	if (&a == &b) return; 	m.lock(); 	std::lock_guard<std::mutex> lockA(a.m_mutex); 	std::lock_guard<std::mutex> lockB(b.m_mutex); 	m.unlock(); 	if (a.Account < money) return; 	a.Account -= money; 	b.Account += money; }  int main() { 	int money = 10; 	BankAccount Tom(100); 	BankAccount Jerry(100); 	std::mutex m; 	std::thread t1([&Tom, &Jerry, money, &m] { 		transferMoney(Tom, Jerry, money, m); 	});  	std::thread t2([&Tom, &Jerry, money, &m] { 		transferMoney(Jerry, Tom, money+30, m); 	}); 	if (t1.joinable()) { 		t1.join(); 	} 	if (t2.joinable()) { 		t2.join(); 	} 	std::cout << "Tom has " << Tom.Account << std::endl; 	std::cout << "Jerry has " << Jerry.Account << std::endl; 	return 0; } 

要么一个都不锁,要么两个都锁住,这样代码就可以顺利跑下去。运行结果如下:
多线程学习(哲学家进餐,生产者消费者模式)
这样就没有任何问题,但是我们仔细看刚才的代码,我们前面说过,我们应该尽量减少手动操作锁的情况,好在C++11中为我们提供了专门的函数实现了对多个锁的同时锁定,具体实现如下:

 void transferMoney(BankAccount& a, BankAccount& b, int money, std::mutex& m) { 	if (&a == &b) return; 	std::lock(a.m_mutex, b.m_mutex); 	std::lock_guard<std::mutex> lockA(a.m_mutex, std::adopt_lock); 	std::lock_guard<std::mutex> lockB(b.m_mutex, std::adopt_lock); 	if (a.Account < money) return; 	a.Account -= money; 	b.Account += money; }  

接下来我们可以看几个leetcode上多线程编程的一些实战:
多线程学习(哲学家进餐,生产者消费者模式)
我们有4个线程同时调用,来交替打印,我们可以使用锁和条件变量来实现,
使用条件变量condition_variable,condition_variable中提供了wait函数

void wait(std::unique_lock<std::mutex>&_lck, _Predicate _Pred); 

其中_Predicate可以加入lamda表达式,在lamda表达式中返回true时,程序继续向下运行,否则线程会被阻塞。另外提供了notify_one和notify_all函数来唤醒一个线程和所有其他被阻塞的线程

先看一个错误的例子:

#include <iostream> #include <thread> #include <mutex> #include <condition_variable>  class FizzBuzz { private:     int n;     std::mutex m;     std::condition_variable cv;     bool fizz_flag = false;     bool buzz_flag = false;     bool fizzbuzz_flag = false; public:     FizzBuzz(int n) {         this->n = n;     }     void printFizz();     void printBuzz();     void printFizzBuzz();     void printNumber(int x);     void fizz();     void buzz();     void fizzbuzz();     void number(); };  void FizzBuzz::printFizz() {     std::cout << " Fizz "; }  void FizzBuzz::printBuzz() {     std::cout << " Buzz "; }  void FizzBuzz::printFizzBuzz() {     std::cout << " FizzBuzz "; }  void FizzBuzz::printNumber(int x) {     std::cout << x ; }  void FizzBuzz::fizz() {     for (int i = 1; i <= n; i++) {         if (i % 3 == 0 && i % 5 != 0) {             std::unique_lock<std::mutex> lock(m);             cv.wait(lock, [this] {                 return fizz_flag;             });             printFizz();             fizz_flag = true;             cv.notify_one();         }     } }  void FizzBuzz::buzz() {     for (int i = 1; i <= n; i++) {         if (i % 3 != 0 && i % 5 == 0) {             std::unique_lock<std::mutex> lock(m);             cv.wait(lock, [this] {                 return buzz_flag;                 });             printBuzz();             buzz_flag = true;             cv.notify_one();         }     } }  void FizzBuzz::fizzbuzz() {     for (int i = 1; i <= n; i++) {         if (i % 3 == 0 && i % 5 == 0) {             std::unique_lock<std::mutex> lock(m);             cv.wait(lock, [this] {                 return fizzbuzz_flag;                 });             printFizzBuzz();             fizzbuzz_flag = true;             cv.notify_one();         }     } }  void FizzBuzz::number() {     for (int i = 1; i <= n; i++) {         if (i % 3!= 0 && i % 5 != 0) {             std::unique_lock<std::mutex> lock(m);             cv.wait(lock, [this] {                 return !fizzbuzz_flag && !fizz_flag && !buzz_flag;                 });             printNumber(i);             fizzbuzz_flag = true;             fizz_flag = true;             buzz_flag = true;             cv.notify_one();         }     } }  int main() {     FizzBuzz f(15);     std::thread t[4];     t[0] = std::thread([&] {f.fizz();});     t[1] = std::thread([&] {f.buzz(); });     t[2] = std::thread([&] {f.fizzbuzz(); });     t[3] = std::thread([&] {f.number(); });     for (auto& cur : t) {         if (cur.joinable()) {             cur.join();         }     }     return 0; } 

我们设置三个标记位,分别来标记打印Fizz,Buzz,FizzBuzz,初始阶段全为false,所以在各自的进程中都会在wait中返回false,故等待,只有number函数可以往下执行,number函数执行结束后将每个标志位置为true,导致其他的进程被唤醒,立马就打印,这样打印出来的不仅时错的,还会发生死锁,输出结果见下图:
多线程学习(哲学家进餐,生产者消费者模式)
那么,我们该如何处理呢?确实我们只需要用一个count来模拟n的变化就可以了。代码如下:

void FizzBuzz::fizz() {     while (count <= n) {         std::unique_lock<std::mutex> lock(m);         cv.wait(lock, [this] {return count > n || (count % 3 == 0 && count % 5 != 0); });         if (count > n) return;         printFizz();         count++;         cv.notify_all();     } }  void FizzBuzz::buzz() {     while (count <= n) {         std::unique_lock<std::mutex> lock(m);         cv.wait(lock, [this] {return count > n || (count % 3 != 0 && count % 5 == 0); });         if (count > n) return;         printBuzz();         count++;         cv.notify_all();     } }  void FizzBuzz::fizzbuzz() {     while (count <= n) {         std::unique_lock<std::mutex> lock(m);         cv.wait(lock, [this] {return count > n || (count % 3 == 0 && count % 5 == 0); });         if (count > n) return;         printFizzBuzz();         count++;         cv.notify_all();     } }  void FizzBuzz::number() {     while (count <= n) {         std::unique_lock<std::mutex> lock(m);         cv.wait(lock, [this] {return count > n || (count % 3 != 0 && count % 5 != 0); });         if (count > n) return;         printNumber(count);         count++;         cv.notify_all();     } } 

代码输出结果如下:
多线程学习(哲学家进餐,生产者消费者模式)
没有任何问题。

最后我们来看一个学习操作系统中的经典问题,哲学家进餐问题:
多线程学习(哲学家进餐,生产者消费者模式)
哲学家进餐问题有三种解决方案:
1.限制哲学家们的就餐数量
2.要么不拿筷子,要么就同时拿两只筷子
3.限制就餐策略

这里我们仅仅对第二种方法进行实现,具体代码如下:

#include <cstdio> #include <thread> #include <mutex> #include <condition_variable>  class DiningPhilosophers {     std::condition_variable cv;     std::mutex m;     bool is_used[5]; public:     DiningPhilosophers() {         for (int i = 0; i < 5; i++) {             is_used[i] = true;         }     }     void pickLeftFork(int philosopher);     void pickRightFork(int philosopher);     void eat(int philosopher);     void putLeftFork(int philosopher);     void putRightFork(int philosopher);     void wantsToEat(int philosopher); };  void DiningPhilosophers::pickLeftFork(int philosopher) {     printf("第%d个哲学家拿起了左边的筷子n", philosopher); }  void DiningPhilosophers::pickRightFork(int philosopher) {     printf("第%d个哲学家拿起了右边的筷子n", philosopher); }  void DiningPhilosophers::eat(int philosopher) {     printf("第%d个哲学家正在用餐n", philosopher); }  void DiningPhilosophers::putLeftFork(int philosopher) {     printf("第%d个哲学家放下了左边的筷子n", philosopher); }  void DiningPhilosophers::putRightFork(int philosopher) {     printf("第%d个哲学家放下了右边的筷子n", philosopher); }   void DiningPhilosophers::wantsToEat(int philosopher) {     int left = philosopher;     int right = (philosopher + 1) % 5;     std::unique_lock<std::mutex> lock(m);     cv.wait(lock, [&] {return is_used[left] && is_used[right]; });     pickLeftFork(philosopher);     pickRightFork(philosopher);     is_used[left] = false;     is_used[right] = false;     eat(philosopher);     putLeftFork(philosopher);     putRightFork(philosopher);     is_used[left] = true;     is_used[right] = true;     cv.notify_all(); }  int main() {     DiningPhilosophers f;     std::thread t[5];     t[0] = std::thread([&] {f.wantsToEat(1);});     t[1] = std::thread([&] {f.wantsToEat(2); });     t[2] = std::thread([&] {f.wantsToEat(3); });     t[3] = std::thread([&] {f.wantsToEat(4); });     t[4] = std::thread([&] {f.wantsToEat(5); });     for (auto& cur : t) {         if (cur.joinable()) {             cur.join();         }     }     return 0; } 

我们定义一个bool数组来判断筷子是否被拿起,起初筷子都没有被拿起,然后一旦拿起,就是拿一双筷子,所以要判断is_used[left] && is_used[right],拿起筷子后置为false,吃完放下筷子置为true,利用condition_variable完成代码实现,输出结果如下:
多线程学习(哲学家进餐,生产者消费者模式)

版权声明:玥玥 发表于 2021-05-04 13:00:14。
转载请注明:多线程学习(哲学家进餐,生产者消费者模式) | 女黑客导航