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 C#
Spring创建的单例对象,存在线程安全问题吗?
Spring框架提供了多种Bean作用域,包括单例(Singleton)、原型(Prototype)、请求(Request)、会话(Session)、全局会话(GlobalSession)等。单例是默认作用域,保证每个Spring容器中只有一个Bean实例;原型作用域则每次请求都会创建一个新的Bean实例;请求和会话作用域分别与HTTP请求和会话绑定,在Web应用中有效。 单例Bean在多线程环境中可能面临线程安全问题,Spring容器虽然确保Bean的创建过程是线程安全的,但Bean的使用安全性需开发者自行保证。保持Bean无状态是最简单的线程安全策略;
|
2月前
|
缓存 Java 容器
多线程环境中的虚假共享是什么?
【8月更文挑战第21天】
25 0
|
3月前
|
设计模式 C++
C++一分钟之-设计模式:工厂模式与抽象工厂
【7月更文挑战第14天】设计模式是解决软件设计问题的通用方案。工厂模式与抽象工厂模式是创建型模式,用于对象创建而不暴露创建逻辑。工厂模式推迟实例化到子类,但过度使用会增加复杂性。抽象工厂则创建相关对象族,但过度抽象可能造成不必要的复杂度。两者均应按需使用,确保设计灵活性。代码示例展示了C++中如何实现这两种模式。
36 3
|
3月前
|
设计模式 安全 C++
C++一分钟之-C++中的设计模式:单例模式
【7月更文挑战第13天】单例模式确保类只有一个实例,提供全局访问。C++中的实现涉及线程安全和生命周期管理。基础实现使用静态成员,但在多线程环境下可能导致多个实例。为解决此问题,采用双重检查锁定和`std::mutex`保证安全。使用`std::unique_ptr`管理生命周期,防止析构异常和内存泄漏。理解和正确应用单例模式能提升软件的效率与可维护性。
34 2
|
3月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
64 1
|
3月前
|
设计模式 存储 安全
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
47 1
|
2月前
|
设计模式 SQL 安全
单例模式大全:细说七种线程安全的Java单例实现,及数种打破单例的手段!
设计模式,这是编程中的灵魂,用好不同的设计模式,能使你的代码更优雅/健壮、维护性更强、灵活性更高,而众多设计模式中最出名、最广为人知的就是Singleton Pattern单例模式。通过单例模式,我们就可以避免由于多个实例的创建和销毁带来的额外开销,本文就来一起聊聊单例模式。
|
3月前
|
Rust 安全 程序员
Rust与C++的区别及使用问题之Rust解决多线程下的共享的问题如何解决
Rust与C++的区别及使用问题之Rust解决多线程下的共享的问题如何解决
|
3月前
|
Rust 编译器 程序员
Rust与C++的区别及使用问题之Rust避免多线程中的lifetime的问题如何解决
Rust与C++的区别及使用问题之Rust避免多线程中的lifetime的问题如何解决
|
3月前
|
设计模式 存储 缓存
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
42 0

热门文章

最新文章

下一篇
无影云桌面