多執行緒學習(哲學家進餐,生產者消費者模式)
在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完成代碼實作,輸出結果如下:

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/282847.html
標籤:其他
