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月前
|
Java API 调度
从阻塞到畅通:Java虚拟线程开启并发新纪元
从阻塞到畅通:Java虚拟线程开启并发新纪元
293 83
|
2月前
|
存储 Java 调度
Java虚拟线程:轻量级并发的革命性突破
Java虚拟线程:轻量级并发的革命性突破
240 83
|
4月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
187 0
|
4月前
|
设计模式 运维 监控
并发设计模式实战系列(4):线程池
需要建立持续的性能剖析(Profiling)和调优机制。通过以上十二个维度的系统化扩展,构建了一个从。设置合理队列容量/拒绝策略。动态扩容/优化任务处理速度。检查线程栈定位热点代码。调整最大用户进程数限制。CPU占用率100%
314 0
|
4月前
|
存储 缓存 安全
JUC并发—11.线程池源码分析
本文主要介绍了线程池的优势和JUC提供的线程池、ThreadPoolExecutor和Excutors创建的线程池、如何设计一个线程池、ThreadPoolExecutor线程池的执行流程、ThreadPoolExecutor的源码分析、如何合理设置线程池参数 + 定制线程池。
JUC并发—11.线程池源码分析
|
7月前
|
缓存 安全 Java
面试中的难题:线程异步执行后如何共享数据?
本文通过一个面试故事,详细讲解了Java中线程内部开启异步操作后如何安全地共享数据。介绍了异步操作的基本概念及常见实现方式(如CompletableFuture、ExecutorService),并重点探讨了volatile关键字、CountDownLatch和CompletableFuture等工具在线程间数据共享中的应用,帮助读者理解线程安全和内存可见性问题。通过这些方法,可以有效解决多线程环境下的数据共享挑战,提升编程效率和代码健壮性。
235 6
|
8月前
|
设计模式 缓存 安全
「全网最细 + 实战源码案例」设计模式——单例设计模式
单例模式是一种创建型设计模式,确保一个类在整个程序运行期间只有一个实例,并提供一个全局访问点来获取该实例。它常用于控制共享资源的访问,如数据库连接、配置管理等。实现方式包括饿汉式(类加载时初始化)、懒汉式(延迟加载)、双重检查锁、静态内部类和枚举单例等。其中,枚举单例最简单且安全,能有效防止反射和序列化破坏。
136 7
|
10月前
|
安全 Java
线程安全的艺术:确保并发程序的正确性
在多线程环境中,确保线程安全是编程中的一个核心挑战。线程安全问题可能导致数据不一致、程序崩溃甚至安全漏洞。本文将分享如何确保线程安全,探讨不同的技术策略和最佳实践。
158 6
|
10月前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
209 8
|
10月前
|
设计模式 前端开发 JavaScript
JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式
本文深入探讨了JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式,结合电商网站案例,展示了设计模式如何提升代码的可维护性、扩展性和可读性,强调了其在前端开发中的重要性。
134 2

热门文章

最新文章