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

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

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

#include 
#include 

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 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 

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,但是在某一种情况下运行结果会是这样的!
多线程学习(哲学家进餐,生产者消费者模式)_第1张图片
程序一直卡在这地方,这是为什么呢?
我们可以这么想,我们有两个线程来实现转账,在高并发的情况下,线程一和线程二同时进行,首先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;
}

要么一个都不锁,要么两个都锁住,这样代码就可以顺利跑下去。运行结果如下:
多线程学习(哲学家进餐,生产者消费者模式)_第2张图片
这样就没有任何问题,但是我们仔细看刚才的代码,我们前面说过,我们应该尽量减少手动操作锁的情况,好在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上多线程编程的一些实战:
多线程学习(哲学家进餐,生产者消费者模式)_第3张图片
我们有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 
#include 
#include 
#include 

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,导致其他的进程被唤醒,立马就打印,这样打印出来的不仅时错的,还会发生死锁,输出结果见下图:
多线程学习(哲学家进餐,生产者消费者模式)_第4张图片
那么,我们该如何处理呢?确实我们只需要用一个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();
    }
}

代码输出结果如下:
在这里插入图片描述
没有任何问题。

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

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

#include 
#include 
#include 
#include 

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完成代码实现,输出结果如下:
多线程学习(哲学家进餐,生产者消费者模式)_第6张图片

你可能感兴趣的:(多线程编程,c++,多线程,并发编程,leetcode)