cpp多线程编程demo2

本文探讨了多线程编程中的竞争条件问题,通过实例展示了如何在Wallet类中使用mutex避免竞态条件,以及如何利用条件变量实现线程间的事件通知。学习了如何使用锁机制和条件变量来确保数据一致性,提高并发程序的正确性和性能。
摘要由CSDN通过智能技术生成

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,或者某任务被另一个线程完成。

例如,我们创建一个基于网络的应用程序,处理如下的任务:

  1. 与服务器器进行一些握手操作;
  2. 从xml文件load数据;
  3. 处理从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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值