在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,但是在某一种情况下运行结果会是这样的!
程序一直卡在这地方,这是为什么呢?
我们可以这么想,我们有两个线程来实现转账,在高并发的情况下,线程一和线程二同时进行,首先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
#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,导致其他的进程被唤醒,立马就打印,这样打印出来的不仅时错的,还会发生死锁,输出结果见下图:
那么,我们该如何处理呢?确实我们只需要用一个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
#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完成代码实现,输出结果如下: