C++并发与多线程(三)单例设计模式与共享数据分析、call_once、condition_variable使用

简介: C++并发与多线程(三)单例设计模式与共享数据分析、call_once、condition_variable使用

单例设计模式

  在整个项目中,有某个或者某些特殊的类,只能创建一个属于该类的对象。单例类:只能生成一个对象。整个项目中,有某个或者某些特殊的类,属于该类的对象,我只能创建1个,多了创建不了。设计代码如下:

#include <iostream> 
#include <mutex>
using namespace std;
mutex myMutex;
//懒汉模式
class Singelton{
public:
  static Singelton* getInstance() {
    // 双重锁定 提高效率
    if (instance == NULL) { // instance == NULL不代表一定没被new过,可能另外一个线程new了。但是这里已经进入循环了。
      lock_guard<mutex> myLockGua(myMutex);
      if (instance == NULL) {
        instance = new Singelton();
      }
    }
    return instance;
  }
private:
  Singelton() {} // 私有化构造函数
  static Singelton* instance; // 静态成员变量
};
Singelton* Singelton::instance = NULL; // 给静态成员变量赋初值。
//饿汉模式
class Singelton2 {
public:
  static Singelton2* getInstance() {
    return instance;
  }
private:
  Singelton2() {} // 私有化构造函数
  static Singelton2 * instance; // 静态成员变量
};
Singelton2* Singelton2::instance = new Singelton2; // new Singelton2()也可以
int main(void)
{
  // 单例类只能通过调用调用接口getInstance()创建,无法通过实例化创建。
  Singelton* singer = Singelton::getInstance();
  Singelton* singer2 = Singelton::getInstance();
  if (singer == singer2)
    cout << "二者是同一个实例" << endl;
  else
    cout << "二者不是同一个实例" << endl;
  cout << "----------   以下 是 饿汉式  ------------" << endl;
  Singelton2* singer3 = Singelton2::getInstance();
  Singelton2* singer4 = Singelton2::getInstance();
  if (singer3 == singer4)
    cout << "二者是同一个实例" << endl;
  else
    cout << "二者不是同一个实例" << endl;
  return 0;
}

  单例设计模式中,对象构造函数是私有成员方法,创建对象的时候只能通过调用接口getInstance()创建,无法通过实例化创建,因为构造函数被私有化了。程序输出结果为:

  如果觉得在单例模式new了一个对象,而没有自己delete掉,这样不合理。可以增加一个类中类CGarhuishounew一个单例类时创建一个静态的CGarhuishou对象,这样在程序结束时会调用CGarhuishou的析构函数,释放掉new出来的单例对象。

class Singelton
{
public:
  static Singelton * getInstance() {
         if (instance == NULL) {
    static CGarhuishou huishou;
    instance = new Singelton;
         }
         return instance;
  }
  class CGarhuishou {
  public:
    ~CGarhuishou()
    {
      if (Singelton::instance)
      {
        delete Singelton::instance;
        Singelton::instance = NULL;
      }
    }
  };
private:
  Singelton() {}
  static Singelton *instance;
};
Singelton * Singelton::instance = NULL;

  单例类的对象可能会被多个线程使用到,一般我们可以在主线程中把该创建的对象创建了,该加载的数据加载了去。但是这种方式面临问题是:需要在自己创建的线程中来创建单例类的对象,并且这种线程可能不止一个。我们可能面临GetInstance()这种成员函数需要互斥的情况。想要解决这个问题的话,我们可以在加锁前判断m_instance是否为空,否则每次调用Singelton::getInstance()都要加锁,十分影响效率。

std::call_once

  std::call_once()是一个函数模板,也是C++11新引入的函数。该函数的第一个参数为标记,第二个参数是一个函数名(比如我们有个参数a函数,那么它的第二个参数就是a())。它的功能是:能够保证函数a()只被调用一次。具备互斥量的能力,而且比互斥量消耗的资源更少,更高效。call_once()需要与一个标记结合使用,这个标记为std::once_flag;其实once_flag是一个结构,call_once()就是通过标记来决定函数是否执行,调用成功后,就把标记设置为一种已调用状态。

#include <iostream>
#include <mutex>
using namespace    std;
mutex myMutex;
//懒汉模式
once_flag g_flag;
class Singelton{
public:
    static Singelton* getInstance() {
        call_once(g_flag, CreateInstance); //两个线程同时执行到这里,其中一个线程要等另外一个线程执行完毕
        return instance;
    }
    static void CreateInstance(){
        instance = new Singelton();
    }
private:
    Singelton() {} // 私有化构造函数
    static Singelton* instance; // 静态成员变量
};
Singelton* Singelton::instance = NULL; // 给静态成员变量赋初值。
//饿汉模式
class Singelton2 {
public:
    static Singelton2* getInstance() {
        return instance;
    }
private:
    Singelton2() {} // 私有化构造函数
    static Singelton2 * instance; // 静态成员变量
};
Singelton2* Singelton2::instance = new Singelton2; // new Singelton2()也可以
int main(void)
{
    // 单例类只能通过调用调用接口getInstance()创建,无法通过实例化创建。
    Singelton* singer = Singelton::getInstance();
    Singelton* singer2 = Singelton::getInstance();
    if (singer == singer2)
        cout << "二者是同一个实例" << endl;
    else
        cout << "二者不是同一个实例" << endl;
    cout << "----------        以下 是 饿汉式    ------------" << endl;
    Singelton2* singer3 = Singelton2::getInstance();
    Singelton2* singer4 = Singelton2::getInstance();
    if (singer3 == singer4)
        cout << "二者是同一个实例" << endl;
    else
        cout << "二者不是同一个实例" << endl;
    return 0;
}

condition_variable

  std::condition_variable实际上是一个类,是一个和条件相关的类,说白了就是等待一个条件达成。比如说线程A等待一个条件满足,线程B完成了这个条件之后就通知线程A让其继续往下执行。看如下代码:

#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A {
public:
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; ++i){
            cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
            {
                //lock_guard<mutex> sbguard(myMutex1);
                lock(myMutex1, myMutex2); // 只有等所有互斥量都锁住才能锁成功。
                //myMutex2.lock(); // 先锁2再锁1,就会产生死锁。
                //myMutex1.lock();
                msgRecvQueue.push_back(i);
                myMutex1.unlock(); // 解锁的时候先解锁哪一个就无所谓。
                myMutex2.unlock();
            }
        }
    }
    bool outMsgLULProc(){
        myMutex1.lock(); // 这里与之前的先锁2后锁1会产生死锁。
        myMutex2.lock();
        if (!msgRecvQueue.empty())
        {
            cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
            msgRecvQueue.pop_front();
            myMutex2.unlock();
            myMutex1.unlock();
            return true;
        }
        myMutex2.unlock();
        myMutex1.unlock();
        return false;
    }
    void outMsgRecvQueue()
    {
        for (int i = 0; i < 100000; ++i){
            if (outMsgLULProc()){
                cout << "outMsgLULProc()执行了,取出一个元素。" << endl;
            }
            else{
                // 消息队列为空
                cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
            }
        }
    }
private:
    list<int> msgRecvQueue;
    mutex myMutex1;
    mutex myMutex2;
};
int main()
{
    A myobja;
    mutex myMutex;
    thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
    thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
    myOutMsgObj.join();
    myInMsgObj.join();
    return 0;
}

  outMsgLULProc函数在不停地加锁,判断是否为空,效率很低,我们可以通过双重锁定,避免先锁一下的低效行为:

bool outMsgLULProc(){
        if (!msgRecvQueue.empty()){
            myMutex1.lock(); // 这里与之前的先锁2后锁1会产生死锁。
            myMutex2.lock();
            if (!msgRecvQueue.empty())
            {
                cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
                msgRecvQueue.pop_front();
                myMutex2.unlock();
                myMutex1.unlock();
                return true;
            }
            myMutex2.unlock();
            myMutex1.unlock();
        }
        return false;
    }

  如果能把程序写成,当有数据的时候就来通知我们,我们再去取数据这种思路就很好。condition_variable类就能帮助我们完成这样一件事情。

std::unique_lock<std::mutex> sbgurad(myMutex);
// wait()用来等一个东西,如果第二个参数的返回值为false,wait将解锁互斥量,并堵塞到本行。
// 堵塞到其它线程调用notify_once()为止。如果不给定第二个参数,那么就与第二个参数返回false效果一样。
cond.wait(sbgurad, [this]{ // 一个lambda表达式,相当于一个可调用对象。
  if(!msgRecvQueue.empty()) return true;
  else return false;
});

  wait()用来等一个东西。如果第二个参数的lambda表达式返回值是false,那么wait()将解锁互斥量,并阻塞到本行。如果第二个参数的lambda表达式返回值是true,那么wait()直接返回并继续执行。阻塞到什么时候为止呢?阻塞到其他某个线程调用notify_one()成员函数为止。如果没有第二个参数,那么效果跟第二个参数lambda表达式返回false效果一样。

cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
std::unique_lock<std::mutex> sbgurad(myMutex);
msgRecvQueue.push_back(i);
cond.notify_one(); // 尝试把outMsgLULProc中的wait线程唤醒。

  当其他线程用notify_one()将本线程wait()唤醒后,这个wait恢复后:1、wait()不断尝试获取互斥量锁,如果获取不到那么流程就卡在wait()这里等待获取,如果获取到了,那么wait()就继续执行,获取到了锁。如果wait有第二个参数就判断这个lambda表达式。a)如果表达式为false,那wait又对互斥量解锁,然后又休眠,等待再次被notify_one()唤醒。b)如果lambda表达式为true,则wait返回,流程可以继续执行(此时互斥量已被锁住)。

std::unique_lock<std::mutex> sbgurad(myMutex);
// wait()用来等一个东西,如果第二个参数的返回值为false,wait将解锁互斥量,并堵塞到本行。
// 堵塞到其它线程调用notify_once()为止。如果不给定第二个参数,那么就与第二个参数返回false效果一样。
cond.wait(sbgurad, [this]{ // 一个lambda表达式,相当于一个可调用对象。
  if(!msgRecvQueue.empty()) return true;
  else return false;
});

  所有代码如下所示:

#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A {
public:
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 1000000000; ++i){
            cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
            std::unique_lock<std::mutex> sbgurad(myMutex);
            msgRecvQueue.push_back(i);
            cond.notify_one(); // 尝试把outMsgRecvQueue中的wait线程唤醒。
        }
    }
    void outMsgRecvQueue()
    {
        while(true){
            std::unique_lock<std::mutex> sbgurad(myMutex);
            // wait()用来等一个东西,如果第二个参数的返回值为false,wait将解锁互斥量,并堵塞到本行。
            // 堵塞到其它线程调用notify_once()为止。如果不给定第二个参数,那么就与第二个参数返回false效果一样。
            cond.wait(sbgurad, [this]{ // 一个lambda表达式,相当于一个可调用对象。
                if(!msgRecvQueue.empty()) return true;
                else return false;
            });
            msgRecvQueue.pop_front();
            cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
            sbgurad.unlock();
        }
    }
private:
    list<int> msgRecvQueue;
    mutex myMutex;
    std::condition_variable cond;
};
int main()
{
    A myobja;
    thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
    thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
    myOutMsgObj.join();
    myInMsgObj.join();
    return 0;
}

  上面的代码可能导致出现一种情况:因为outMsgRecvQueue()inMsgRecvQueue()并不是一对一执行的,所以当程序循环执行很多次以后,可能在msgRecvQueue中已经有了很多消息,但是,outMsgRecvQueue还是被唤醒一次只处理一条数据。这时可以考虑outMsgRecvQueue多执行几次,或者对inMsgRecvQueue进行限流。

  • notify_one():通知一个线程的wait()notify_all():通知所有线程的wait()

虚假唤醒

  notify_one或者notify_all唤醒wait()后,实际有些线程可能不满足唤醒的条件,就会造成虚假唤醒,可以在wait中再次进行判断解决虚假唤醒。如下代码中inMsgRecvQueue收到数据之后,通过notify_one通知其它线程,其它线程在wait()函数处等待,条件满足之后往下执行。

#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A {
public:
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 1000000; ++i){
            cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
            std::unique_lock<std::mutex> sbgurad(myMutex);
            msgRecvQueue.push_back(i);
            cond.notify_one(); // 尝试把outMsgRecvQueue中的wait线程唤醒。
        }
    }
    void outMsgRecvQueue()
    {
        while(true){
            std::unique_lock<std::mutex> sbgurad(myMutex);
            // wait()用来等一个东西,如果第二个参数的返回值为false,wait将解锁互斥量,并堵塞到本行。
            // 堵塞到其它线程调用notify_once()为止。如果不给定第二个参数,那么就与第二个参数返回false效果一样。
            cond.wait(sbgurad, [this]{ // 一个lambda表达式,相当于一个可调用对象。
                if(!msgRecvQueue.empty()) return true;
                else return false;
            });
            msgRecvQueue.pop_front();
            cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
            sbgurad.unlock();
        }
    }
private:
    list<int> msgRecvQueue;
    mutex myMutex;
    std::condition_variable cond;
};
int main()
{
    A myobja;
    thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
    thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
    myOutMsgObj.join();
    myInMsgObj.join();
    return 0;
}

  如果往数据中插入一条数据,却多次调用notify_one()的话(因为有时候我们需要确保数据中有元素时,wait函数能够被唤醒),我们就可能存在虚假唤醒的情况。解决:wait中要有第二个参数(lambda),并且这个lambda中要正确判断所处理的公共数据是否存在。比如上述代码中的:

cond.wait(sbgurad, [this]{ // 一个lambda表达式,相当于一个可调用对象。
                if(!msgRecvQueue.empty()) return true;
                else return false;
});

  我们就是通过if(!msgRecvQueue.empty())来判断条件是否满足。

参考

https://blog.csdn.net/qq_38231713/article/details/106092538

相关文章
|
2月前
|
安全
List并发线程安全问题
【10月更文挑战第21天】`List` 并发线程安全问题是多线程编程中一个非常重要的问题,需要我们认真对待和处理。只有通过不断地学习和实践,我们才能更好地掌握多线程编程的技巧和方法,提高程序的性能和稳定性。
249 59
|
2月前
|
安全 Java
线程安全的艺术:确保并发程序的正确性
在多线程环境中,确保线程安全是编程中的一个核心挑战。线程安全问题可能导致数据不一致、程序崩溃甚至安全漏洞。本文将分享如何确保线程安全,探讨不同的技术策略和最佳实践。
58 6
|
2月前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
85 7
|
2月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
2月前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
2月前
|
消息中间件 存储 安全
|
3月前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
|
3月前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
45 1
|
5月前
|
数据采集 数据可视化 数据挖掘
数据分析大神养成记:Python+Pandas+Matplotlib助你飞跃!
在数字化时代,数据分析至关重要,而Python凭借其强大的数据处理能力和丰富的库支持,已成为该领域的首选工具。Python作为基石,提供简洁语法和全面功能,适用于从数据预处理到高级分析的各种任务。Pandas库则像是神兵利器,其DataFrame结构让表格型数据的处理变得简单高效,支持数据的增删改查及复杂变换。配合Matplotlib这一数据可视化的魔法棒,能以直观图表展现数据分析结果。掌握这三大神器,你也能成为数据分析领域的高手!
100 2
|
5月前
|
机器学习/深度学习 数据采集 数据可视化
基于爬虫和机器学习的招聘数据分析与可视化系统,python django框架,前端bootstrap,机器学习有八种带有可视化大屏和后台
本文介绍了一个基于Python Django框架和Bootstrap前端技术,集成了机器学习算法和数据可视化的招聘数据分析与可视化系统,该系统通过爬虫技术获取职位信息,并使用多种机器学习模型进行薪资预测、职位匹配和趋势分析,提供了一个直观的可视化大屏和后台管理系统,以优化招聘策略并提升决策质量。
283 4