cpp多线程编程demo2
数据共享和竞争
在多线程环境中,线程间的数据共享很简单,但是在程序中这种简单的数据共享可能会引起问题,其中一种便是竞争条件。
什么是竞争条件?
- 竞争条件是发生在多线程应用程序中的一种bug
- 当两个或多个线程并行执行一组操作,访问相同的内存位置,此时,它们中的一个或多个线程会修改内存位置中的数据,这可能会导致一些意外的结果,这就是竞争条件。
竞争条件通常较难发现并重现,因为它们并不总是出现,只有当两个或多个线程执行操作的相对顺序导致意外结果时,它们才会发生,通过例子理解:
创建5个线程,这些线程共享类Wallet的一个对象,使用addMoney()成员函数并行添加1000内部资金。
所以,如果最初钱包中的钱是0,那么在所有线程的竞争执行完毕后,钱包中的钱应该是5000,但是,由于所有线程同时修改共享数据,在某些情况下,钱包中的钱可能远小于5000。
测试如下:
#include <iostream>
#include <thread>
#include <algorithm>
#include <vector>
class Wallet {
int mMoney;
public:
Wallet() : mMoney(0) { }
int getMoney() { return mMoney; }
void addMoney(int money) {
for (int i = 0; i < money; i++) {
mMoney++;
}
}
};
int testMultithreadWallet() {
Wallet walletObject;
std::vector<std::thread> threads;
for (int i = 0; i < 5; i++) {
threads.push_back(std::thread(&Wallet::addMoney, &walletObject, 1000));
}
for (int i = 0; i < 5; i++) {
threads.at(i).join();
}
return walletObject.getMoney();
}
int main() {
int val = 0;
for (int k = 0; k < 1000; k++) {
if ((val = testMultithreadWallet()) != 5000) {
std::cout << "Error at count = " << k << " Money in Wallet = " << val << std::endl;
}
}
return 0;
}
由于相同Wallet类对象的成员函数addMoney()执行了5次,所以money预计为50000,但由于addMoney()成员函数并行执行,因此在某些情况下,mMoney可能远小于5000
输出:
Error at count = 971 Money in Wallet = 4568
Error at count = 971 Money in Wallet = 4568
Error at count = 972 Money in Wallet = 4260
Error at count = 972 Money in Wallet = 4260
Error at count = 973 Money in Wallet = 4976
Error at count = 973 Money in Wallet = 4976
这种现象的原因:
每个线程并行地增加相同的成员变量“mMoney”,看似是一条线,但是这个“nMoney++”实际上被转换为3条机器命令:
- 在Register中加载"mMoney"变量
- 增加register的值
- 用register的值更新“mMoney”变量
现在,假设在特定的情况下,上述命令执行程序如下:
线程1: | 线程2: |
---|---|
在寄存器中加载“mMoney”变量 | |
在寄存器中加载“mMoney”变量 | |
增加寄存器的值 | |
增加寄存器的值 | |
使用寄存器中的值更新“mMoney”变量 | |
使用寄存器中的值更新“mMoney”变量 |
在这种情况下,一个增量将被忽略,因为不是增加mMoney变量两次,而是增加不同的寄存器,“mMoney”变量的值被覆盖。
假设在这种情况前,mMoney的值是46.如上图所示,它增加了2次,所以预期结果是48.但是由于上述情况下的竞争条件,mMoney最终的值仅为47
这就叫做竞争条件。
怎样修复这种竞争条件
为了解决这个问题,我们需要使用lock机制,每个线程需要在修改或读取共享数据之前获取一个锁,并且在操作完成后,将该锁释放。
使用mutex修复竞争
这节我们讨论怎样使用mutex锁保护多线程环境中的共享数据来避免竞争条件
为了修复多线程环境中的竞争条件,我们需要mutex互斥锁,在修改或读取共享数据前,需要对数据加锁,修改完成后,对数据进行解锁。
在c++11的线程库中,mutexes在头文件中,表示互斥体的类是std::mutex。
mutex有两个重要的方法:
1.) lock()
2.) unlock()
我们在多线程类Wallet中使用mutex来避免竞争条件。
Wallet类提供了在Wallet中增加money的方法,并且在不同的线程中使用相同的Wallet对象,所以我们需要对Wallet的addMoney()方法加锁。在增加Wallet中的money前加锁,并且在离开该函数前解锁,看代码:Wallet类内部维护money,并提供函数addMoney(),这个成员函数首先获取一个锁,然后给wallet对象的money增加指定的数额,最后释放锁
现创建5个线程,这些线程共享相同的Wallet类对象,并使用其addMoney()成员函数并行增加1000内部的money。
所以,如果wallet的初始money是0,当所有线程完成对Wallet中的money的操作后,其值应该是5000。这个mutex锁就是用来保证最终Wallet中的money的值是5000。
测试如下:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
class Wallet {
int mMoney;
std::mutex mutex;
public:
Wallet() : mMoney(0) { }
int getMoney() { return mMoney;}
void addMoney(int money) {
mutex.lock();
for (int i = 0; i < money; i++) {
mMoney++;
}
mutex.unlock();
}
};
int testMultithreadWallet() {
Wallet walletObject;
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.push_back(std::thread(&Wallet::addMoney, &walletObject, 1000));
}
for (int i = 0; i < threads.size(); i++) {
threads.at(i).join();
}
return walletObject.getMoney();
}
int main() {
int val = 0;
for (int k = 0; k < 1000; k++) {
if ((val = testMultithreadWallet()) != 5000) {
std::cout << "Error at count= " << k << " money in wallet" << val << std::endl;
}
}
return 0;
}
这种情况保证了钱包里的钱不会出现少于5000的情况,因为addMoney()中的互斥锁确保了只有在一个线程修改完成money后,另一个线程才能对其进行修改,但是,如果我们忘记在函数结束后对锁进行释放会怎么样?这种情况下,一个线程将退出而不释放锁,其他线程将保持等待,为了避免这种情况,我们应当使用std::lock_guard。
std::lock_guard
是一个template class,它为mutex实现RALL,它将mutex包裹在其对象内,并将附加的mutex锁定在其构造函数中,当其析构函数被调用时,它将释放互斥体。
代码:
#include <mutex>
#include <iostream>
#include <thread>
#include <vector>
class Wallet {
int mMoney;
std::mutex mutex;
public:
Wallet() : mMoney(0) { }
int getMoney() { return mMoney;}
void addMoney(int money) {
std::lock_guard<std::mutex> lockGuard(mutex);
for (int i = 0; i < money; ++i) {
//如果在此处发生异常,lockGuadr的析构函数将会因为堆栈展开而被调用
mMoney++;
}
}//一旦函数退出,那么lockGuard对象的析构函数将被调用,在析构函数中mutex会被释放
};
int testMultithreadWallet() {
Wallet walletObject;
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.push_back(std::thread(&Wallet::addMoney, &walletObject, 1000));
}
for (int i = 0; i < threads.size(); i++) {
threads.at(i).join();
}
return walletObject.getMoney();
}
int main() {
int val = 0;
for (int k = 0; k < 1000; k++) {
if ((val = testMultithreadWallet()) != 5000) {
std::cout << "Error at count= " << k << " money in wallet" << val << std::endl;
}
}
return 0;
}
这样,再用上面那个例子测试就没有问题。
条件变量
事件处理
本节讨论在多线程环境下的事件处理。有时,线程需要等待某事件发生,比如一个条件变为true,或者某任务被另一个线程完成。
例如,我们创建一个基于网络的应用程序,处理如下的任务:
- 与服务器器进行一些握手操作;
- 从xml文件load数据;
- 处理从xml文件load的数据.
可以发现,任务1不依赖其他的任务,而任务3则依赖于任务2,这意味着任务1和任务2可以由不同的线程并行运行,以提升程序性能。
因此,让我们将其分解成一个多线程的应用程序
现在,它包含2个线程,线程1的任务是:
- 初始化线程2
- 与服务器进行握手操作
- 等待线程2从xml获取的数据
- 处理从xml获取的数据
线程2的任务是:
- 从xml获取数据
- 通知另一个线程,即数据已到位
在上图中,线程1处理一些操作,然后等待event发生,这event是 “数据是否成功获取”,一旦线程1收到该event,那么它将对数据进行处理。
- 当线程1忙于处理握手机制时,线程2并行地获取数据。
- 当线程2成功从xml处获取数据后,它将通过对event发信号来通知线程1。
- 当event发出信号时,线程1将继续处理数据。
实现方式
选项1:创建一个默认为false的boolean型全局变量,在线程2中将其设为true,线程1将会循环检测其值,一旦该值被设为true,线程1将会继续处理数据,由于它是一个由两个线程共享的全局变量,需要使用mutex锁进行同步。
代码如下:
#include <iostream>
#include <thread>
#include <mutex>
class Application
{
std::mutex m_mutex;
bool m_bDataLoaded;
public:
Application(){
m_bDataLoaded = false;
}
void loadData(){
//使该线程sleep 1秒
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
std::cout<<"Load Dada from Xml"<<std::endl;
//锁定数据
std::lock_guard<std::mutex> guard(m_mutex);
//flag设为true,表明数据已加载
m_bDataLoaded = true;
}
void mainTask(){
std::cout<<"Do some Handshaking"<<std::endl;
//获得锁
m_mutex.lock();
//检测flag是否设为true
while (m_bDataLoaded != true) {
//释放锁
m_mutex.unlock();
//sleep 100ms
std::this_thread::sleep_for(std::chrono::milliseconds(100));
//获取锁
m_mutex.lock();
}
m_mutex.unlock();
//处理加载的数据
std::cout<<"Do Processing On loaded Data"<<std::endl;
}
};
int main(){
Application app;
std::thread thread_1(&Application::mainTask, &app);
std::thread thread_2(&Application::loadData, &app);
thread_2.join();
thread_1.join();
return 0;
}
输出:
Do some Handshaking
Load Dada from Xml
Do Processing On loaded Data
该方法存在以下缺陷:为了检测变量,线程将会持续获取-释放锁,这样会消耗CPU周期并且使线程1变慢,因为它需要获取相同的锁来更新bool变量。因此,显然我们需要一个更好的实现机制,如某种方式,线程1可以通过等待event信号来阻塞,另一个线程可以通知该event并使线程1继续。这将会有相同的CPU周期,并有更好的性能。
选项2:我们可以使用条件变量来实现,条件变量是一种用于在2个线程之间进行信令的事件,一个线程可以等待它得到信号,其他的线程可以给它发信号。
下一节会详细说明这个条件变量,并使用条件变量来解决问题。
条件变量是一种用于在2个线程之间进行信令的事件,一个线程可以等待它得到信号,其他的线程可以给它发信号。
在c++11中,条件变量需要头文件:#include <condition_variable>
同时,条件变量还需要一个mutex锁
条件变量实际上是如何运作的
- 线程1调用等待条件变量,内部获取mutex互斥锁并检查是否满足条件;
- 如果没有,则释放锁,并等待条件变量得到发出的信号(线程被阻塞),条件变量的wait()函数以原子方式提供这两个操作;
- 另一个线程,如线程2,当满足条件时,向条件变量发信号;
- 一旦线程1正等待其恢复的条件变量发出信号,线程1便获取互斥锁,并检查与条件变量相关关联的条件是否满足,或者是否是一个上级调用,如果多个线程正在等待,那么notify_one将只解锁一个线程;
- 如果是一个上级调用,那么它再次调用wait()函数。
条件变量的主要成员函数是:
Wait()
- 它使得当前线程阻塞,直到条件变量得到信号或发生虚假唤醒;
- 它原子性地释放附加的mutex,阻塞当前线程,并将其添加到等待当前条件变量对象的线程列表中,当某线程在同样的条件变量上调用notify_one() 或者 notify_all(),线程将被解除阻塞;
- 这种行为也可能是虚假的,因此,解除阻塞后,需要再次检查条件;
- 一个回调函数会传给该函数,调用它来检查其是否是虚假调用,还是确实满足了真实条件;
- 当线程解除阻塞后,wait()函数获取mutex锁,并检查条件是否满足,如果条件不满足,则再次原子性地释放附加的mutex,阻塞当前线程,并将其添加到等待当前条件变量对象的线程列表中。
notify_one() 如果所有线程都在等待相同的条件变量对象,那么notify_one会取消阻塞其中一个等待线程。
notify_all()
- 如果所有线程都在等待相同的条件变量对象,那么notify_all会取消阻塞所有的等待线程。
如何处理讨论的带有条件变量的多线程情景呢?
#include <iostream>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>
using namespace std::placeholders;
class Application {
std::mutex m_mutex;
std::condition_variable m_condVar;
bool m_bDataLoaded;
public:
Application() {
m_bDataLoaded = false;
}
void loadData() {
//使该线程sleep 1秒
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
std::cout << "Loading Data from XML" << std::endl;
//锁定数据
std::lock_guard<std::mutex> guard(m_mutex);
//flag设为true,表明数据已加载
m_bDataLoaded = true;
//通知条件变量
m_condVar.notify_one();
}
bool isDataLoaded() {
return m_bDataLoaded;
}
void mainTask() {
std::cout << "Do some handshaking" << std::endl;
//获取锁
std::unique_lock<std::mutex> mlock(m_mutex);
//开始等待条件变量得到信号
//wait()将在内部释放锁,并使线程阻塞
//一旦条件变量发出信号,则恢复线程并再次获取锁
//然后检测条件是否满足,如果条件满足,则继续,否则再次进入wait
m_condVar.wait(mlock, std::bind(&Application::isDataLoaded, this));
std::cout << "Do Processing On loaded Data" << std::endl;
}
};
int main() {
Application app;
std::thread thread_1(&Application::mainTask, &app);
std::thread thread_2(&Application::loadData, &app);
thread_2.join();
thread_1.join();
return 0;
}