如何写一个线程安全的单例模式?

单例模式的简单实现

单例模式大概是流传最为广泛的设计模式之一了。一份简单的实现代码大概是下面这个样子的:

class singleton
{
public:
static singleton* instance()
{
if (inst_ != nullptr) {
inst_ = new singleton();
}
return inst_;
}
private:
singleton(){}
static singleton* inst_;
}; singleton* singleton::inst_ = nullptr;

这份代码在单线程的环境下是完全没有问题的,但到了多线程的世界里,情况就有一点不同了。考虑以下执行顺序:

  1. 线程1执行完if (inst_ != nullptr)之后,挂起了;
  2. 线程2执行instance函数:由于inst_还未被赋值,程序会inst_ = new singleton()语句;
  3. 线程1恢复,inst_ = new singleton()语句再次被执行,单例句柄被多次创建。

所以,这样的实现是线程不安全的。

有问题的双重检测锁

解决多线程的问题,最常用的方法就是加锁呗。于是很容易就可以得到以下的实现版本:

class singleton
{
public:
static singleton* instance()
{
guard<mutex> lock{ mut_ };
if (inst_ != nullptr) {
inst_ = new singleton();
}
return inst_;
}
private:
singleton(){}
static singleton* inst_;
static mutex mut_;
}; singleton* singleton::inst_ = nullptr;
mutex singleton::mut_;

这样问题是解决了,但性能上就不那么另人满意,毕竟每一次使用instance都多了一次加锁和解锁的开销。更关键的是,这个锁也不是每次都需要啊!实际我们只有在创建单例实例的时候才需要加锁,之后使用的时候是完全不需要锁的。于是,有人提出了一种双重检测锁的写法:

...
static singleton* instance()
{
if (inst_ != nullptr) {
guard<mutex> lock{ mut_ };
if (inst_ != nullptr) {
inst_ = new singleton();
}
}
return inst_;
}
...

我们先判断一下inst_是否已经初始化了,如果没有,再进行加锁初始化流程。这样,虽然代码看上去有点怪异,但好像确实达到了只在创建单例时才引入锁开销的目的。不过遗憾的是,这个方法是有问题的。Scott Meyers 和 Andrei Alexandrescu 两位大神在C++ and the Perils of Double-Checked Locking 一文中对这个问题进行了非常详细地讨论,我们在这儿只作一个简单的说明,问题出在:

	inst_ = new singleton();

这一行。这句代码不是原子的,它通常分为以下三步:

  1. 调用operator new为singleton对象分配内存空间;
  2. 在分配好的内存空间上调用singleton的构造函数;
  3. 将分配的内存空间地址赋值给inst_。

如果程序能严格按照1-->2-->3的步骤执行代码,那么上述方法没有问题,但实际情况并非如此。编译器对指令的优化重排、CPU指令的乱序执行(具体示例可参考《【多线程那些事儿】多线程的执行顺序如你预期吗?》)都有可能使步骤3执行早于步骤2。考虑以下的执行顺序:

  1. 线程1按步骤1-->3-->2的顺序执行,且在执行完步骤1,3之后被挂起了;
  2. 线程2执行instance函数获取单例句柄,进行进一步操作。

由于inst_在线程1中已经被赋值,所以在线程2中可以获取到一个非空的inst_实例,并继续进行操作。但实际上单例对像的创建还没有完成,此时进行任何的操作都是未定义的。

现代C++中的解决方法

在现代C++中,我们可以通过以下几种方法来实现一个即线程安全、又高效的单例模式。

使用现代C++中的内存顺序限制

现代C++规定了6种内存执行顺序。合理的利用内存顺序限制,即可避免代码指令重排。一个可行的实现如下:

class singleton {
public:
static singleton* instance()
{
singleton* ptr = inst_.load(memory_order_acquire);
if (ptr == nullptr) {
lock_guard<mutex> lock{ mut_ };
ptr = inst_.load(memory_order_relaxed);
if (ptr == nullptr) {
ptr = new singleton();
inst_.store(ptr, memory_order_release);
}
} return inst_;
}
private:
singleton(){};
static mutex mut_;
static atomic<singleton*> inst_;
}; mutex singleton::mut_;
atomic<singleton*> singleton::inst_;

来看一下汇编代码:

可以看到,编译器帮我们插入了必要的语句来保证指令的执行顺序。

使用现代C++中的call_once方法

call_once也是现代C++中引入的新特性,它可以保证某个函数只被执行一次。使用call_once的代码实现如下:

class singleton
{
public:
static singleton* instance()
{
if (inst_ != nullptr) {
call_once(flag_, create_instance);
}
return inst_;
}
private:
singleton(){}
static void create_instance()
{
inst_ = new singleton();
}
static singleton* inst_;
static once_flag flag_;
}; singleton* singleton::inst_ = nullptr;
once_flag singleton::flag_;

来看一下汇编代码:

可以看到,程序最终调用了__gthrw_pthread_once来保证函数只被执行一次。

使用静态局部变量

现在C++对变量的初始化顺序有如下规定:

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

所以我们可以简单的使用一个静态局部变量来实现线程安全的单例模式:

class singleton
{
public:
static singleton* instance()
{
static singleton inst_;
return &inst_;
}
private:
singleton(){}
};

来看一下汇编代码:

可以看到,编译器已经自动帮我们插入了相关的代码,来保证静态局部变量初始化的多线程安全性。

全文完。

【多线程那些事儿】如何使用C++写一个线程安全的单例模式?的更多相关文章

  1. 用 Java 写一个线程安全的单例模式(Singleton)?

    请参考答案中的示例代码,这里面一步一步教你创建一个线程安全的 Java 单例类.当我们说线程安全时,意思是即使初始化是在多线程环境中,仍然能保证单个实例.Java 中,使用枚举作为单例类是最简单的方式 ...

  2. 死磕 java线程系列之自己动手写一个线程池

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写 ...

  3. 手写一个线程池,带你学习ThreadPoolExecutor线程池实现原理

    摘要:从手写线程池开始,逐步的分析这些代码在Java的线程池中是如何实现的. 本文分享自华为云社区<手写线程池,对照学习ThreadPoolExecutor线程池实现原理!>,作者:小傅哥 ...

  4. 死磕 java线程系列之自己动手写一个线程池(续)

    (手机横屏看源码更方便) 问题 (1)自己动手写的线程池如何支持带返回值的任务呢? (2)如果任务执行的过程中抛出异常了该怎么处理呢? 简介 上一章我们自己动手写了一个线程池,但是它是不支持带返回值的 ...

  5. 在Linux下写一个线程池以及线程池的一些用法和注意点

    -->线程池介绍(大部分来自网络)  在这个部分,详细的介绍一下线程池的作用以及它的技术背景以及他提供的一些服务等.大部分内容来自我日常生活中在网络中学习到的一些概念性的东西. -->代码 ...

  6. 原来热加载如此简单,手动写一个 Java 热加载吧

    1. 什么是热加载 热加载是指可以在不重启服务的情况下让更改的代码生效,热加载可以显著的提升开发以及调试的效率,它是基于 Java 的类加载器实现的,但是由于热加载的不安全性,一般不会用于正式的生产环 ...

  7. python_way ,day11 线程,怎么写一个多线程?,队列,生产者消费者模型,线程锁,缓存(memcache,redis)

    python11 1.多线程原理 2.怎么写一个多线程? 3.队列 4.生产者消费者模型 5.线程锁 6.缓存 memcache redis 多线程原理 def f1(arg) print(arg) ...

  8. 用JAVA写一个多线程程序,写四个线程,其中二个对一个变量加1,另外二个对一个变量减1

    package com.ljn.base; /** * @author lijinnan * @date:2013-9-12 上午9:55:32 */ public class IncDecThrea ...

  9. 迅雷笔试题 (JAVA多线程)启动三个线程,分别打印A B C,现在写一个程序 循环打印ABCABCABC

    题目:http://wenku.baidu.com/view/d66187aad1f34693daef3e8a.html 启动三个线程,分别打印A B C,现在写一个程序 循环打印ABCABCABC. ...

随机推荐

  1. Vue 监视数据总结 && 表单控件使用总结

    1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8" /> 5 & ...

  2. 高考集训讲课(To 高一)

    高考集训讲课(To 高一) 主要是怕下午讲着讲着把自己讲懵了,有一定的迷糊概率 经过机房的讨论,一致认为插头\(DP\)实用性不大,所以这次不讲了,先把重要的讲一讲. 顺便吐槽一下,凭什么另外几个人都 ...

  3. 循环结构——while、do-while、for循环

    1.while循环 语法格式: while(条件判断){ 循环体 } 解释: (1)关键字while后的小括号中的内容时循环条件. (2)循环条件是一个布尔表达式,它的值为布尔类型 "真&q ...

  4. Java面试题(四)--RabbitMQ

    1.MQ有哪些使用场景?(高频) 异步处理:用户注册后,发送注册邮件和注册短信.用户注册完成后,提交任务到 MQ,发送模块并行获取 MQ 中的任务. 系统解耦:比如用注册完成,再加一个发送微信通知.只 ...

  5. OpenCV4之C++入门详解

    OpenCV之C++入门 1.Visual Studio安装及环境配置与搭建 下载地址:https://my.visualstudio.com/Downloads?q=Visual,下载后按照说明安装 ...

  6. powershell 执行策略

    前言 上一篇博文,我介绍了一下powershell和cmd的对比.通过学习,我发现powershell的确比cmd更加power,也更加适应现在的使用场景. 那么本文将继续介绍一个powershell ...

  7. javaScript 事件循环机制

    JavaScript是单线程的编程语言,只能同一时间内做一件事.但是在遇到异步事件的时候,js线程并没有阻塞,还会继续执行,这就是因为JS有事件循环机制. 事件循环流程总结 主线程开始执行一段代码, ...

  8. 实现一个会动的鸿蒙 LOGO

    本文将带大家简单实现一个会动的鸿蒙 LOGO. emmm,写本文的动机是之前在掘金看到一篇实现鸿蒙 LOGO 的文章 -- 产品经理:鸿蒙那个开场动画挺帅的 给咱们页面也整一个呗 鸿蒙的 LOGO 本 ...

  9. 文件分享工具ShareLocalFile不需要云盘的实时上传下载文件的云盘工具可以搜索整个网络的文件

    工具的下载地址:https://comm.zhaimaojun.cn/AllSources/ToolDetail/?tid=9693 这是一个未来的项目,可以分享我们的文件,目前由于个人的技术水平限制 ...

  10. P7727 风暴之眼 Eye of the Storm (树形 DP)

    谨 以 此 文 表 达 笔 者 个 人 观 点 , 如 有 冒 犯 官 解 , 可 在 评 论 区 诉 说 _{^{_{谨以此文表达笔者个人观点,如有冒犯官解,可在评论区诉说}}} 谨以此文表达笔者个 ...