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

单例模式的简单实现

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

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. 利用MySQL Router构建读写分离MGR集群

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. 目录 1. 部署MySQL Router 2. 启动mysqlrouter服务 3. 确认读写分离效果 4. 确认只读负载 ...

  2. 技术分享 | Update更新慢、死锁等问题的排查思路分享

    欢迎来到 GreatSQL社区分享的MySQL技术文章,如有疑问或想学习的内容,可以在下方评论区留言,看到后会进行解答 一.简介 在开始排错之前我们需要知道 Update 在 MySQL 中的生命周期 ...

  3. 一文详解 implementation api embed

    最近使用 Android Studio 从事项目开发时,发现对 implementation.api.embed 的用法了解的不是很清楚,这里准备一篇文章对其使用场景或者说是使用方式进行一个总结. d ...

  4. 手把手教你 Apache DolphinScheduler 本地开发环境搭建 | 中英文视频教程

    点击上方 蓝字关注我们 最近,一些小伙伴反馈对小海豚的本地开发环境搭建过程不太了解,这不就有活跃的贡献者送来新鲜的视频教程!在此感谢@Tianqi-Dotes 的细致讲解 贡献者还贴心地录制了中英文两 ...

  5. 《吐血整理》进阶系列教程-拿捏Fiddler抓包教程(12)-Fiddler设置IOS手机抓包,你知多少???

    1.简介 Fiddler不但能截获各种浏览器发出的 HTTP 请求,也可以截获各种智能手机发出的HTTP/ HTTPS 请求. Fiddler 能捕获Android 和 Windows Phone 等 ...

  6. MapReduce入门实战

    MapReduce 思想 MapReduce 是 Google 提出的一个软件架构,用于大规模数据集的并行运算.概率"Map(映射)"和"Reduce(归约)" ...

  7. Go语言 context包源码学习

    你必须非常努力,才能看起来毫不费力! 微信搜索公众号[ 漫漫Coding路 ],一起From Zero To Hero ! 前言 日常 Go 开发中,Context 包是用的最多的一个了,几乎所有函数 ...

  8. .NET异步编程模式(一)

    .NET 提供了三种异步编程模型 TAP - task-based asynchronous pattern APM - asynchronous programming model EAP - ev ...

  9. ceph 008 ceph多区域网关(ceph对象容灾) cephfs文件系统

    clienta作为集群的管理人员.一部分.他是需要秘钥与配置文件的 但真正服务端只需要通过curl就好 ceph 多区域网关 对象存储容灾解决方案 zone与zone会做数据同步. 把会做同步的rgw ...

  10. HTTPS 一定是安全的吗?

    大家好,我是小林. 上周有位读者在面字节时被问道这么一个问题:HTTPS 一定安全可靠吗? 这个问题的场景是这样的:客户端通过浏览器向服务端发起 HTTPS 请求时,被「假基站」转发到了一个「中间人服 ...