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

单例模式的简单实现

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

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. ToString()格式化输出

    C 货币 2.5.ToString("C") ¥2.50 D 十进制数 25.ToString("D5") 00025 E 科学型 25000.ToString ...

  2. 在DELL服务器上安装windows2012 r2服务器系统

    主要过程: 1.准备安装光盘,开启服务器,当出现画面按F10进入服务器自带光盘系统安装向导.(若没有系统光盘,可以用软蝶通刻一个服务系统到+R的光盘).进入后选择设置和安装系统. 2.开始安装前,提示 ...

  3. 技术分享 | check(col_name<>'')为何把空格拒之门外

    1.问题描述 前两天在群里看到同事反馈一个空格问题,大致现象如下: mysql> select @@version; +-----------+ | @@version | +--------- ...

  4. 自己做一个RTOS

    什么是操作系统?其实就是一个程序, 这个程序可以控制计算机的所有资源,对资源进行分配,包括CPU时间,内存,IO端口等,按一定规则分配给所需要的进程(进程?也就是一个程序,可以单独执行),并且自动控制 ...

  5. ArkUI block 标签

    block 标签在构建时不会被当作真实 DOM,而 block 标签之下的标签都会被渲染到真实 DOM 中.它类似于 Vue 的 template 标签. <div class="co ...

  6. linux scsi相关的一些学习笔记

      最近看scsi相关处理的一些备忘,比较零碎,仅作参考. 先从最显而易见的打印入手: [0:0:0:0] disk ATA INTEL SSDSC2BX20 0150 - [0:0:1:0] dis ...

  7. StarRocks 运维工具 StarGo

    注:本文主要内容均来源 StarRocks 官网 https://docs.starrocks.com/zh-cn/main/administration/stargo StarGo 是一个用于管理多 ...

  8. 「雅礼集训 2017 Day7」跳蚤王国的宰相(树的重心)

    题面 来源 「 雅 礼 集 训 2017 D a y 7 」 跳 蚤 王 国 的 宰 相   传 统 2000   m s 1024   M i B {\tt「雅礼集训 2017 Day7」跳蚤王国的 ...

  9. 【MIDO】乐理基础 与 python - 从零开始到编写柱式和弦与分解和弦

    本篇文章从律学开始,从十二平均律出发,介绍一些基础必要的乐理知识,然后编写python文件,输出和弦音频文件. 乐理知识部分: 一.律学简述(temperament)   1.概论   律学,又称&q ...

  10. 使用IntelliJ IDEA新建一个spring boot项目

    好家伙, 使用IntelliJ IDEA新建一个spring boot项目 目的很简单,就是网页上出现一个"hello world" 别的暂时不管 首先关于工具IntelliJ I ...