多线程学习(哲学家进餐,生产者消费者模式)
在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();