多线程中的单件模式

本贴最后更新于 2642 天前,其中的信息可能已经时移俗易

单件模式可能是所有设计模式中最简单的一个了,但在 C++ 中,尤其是还要支持多线程的话,要想写一个正确的实现却并不容易,不信请看:

class CSingleton { public: CSingleton() { _tprintf( _T("CSingleton::Constructor: Before Sleep\n") ); Sleep( 1000 ); // 不会改变逻辑, 但增大了问题出现的概率 _tprintf( _T("CSingleton::Constructor: After Sleep\n") ); } void DoSomeThing() { _tprintf( _T("CSingleton::DoSomeThing\n") ); } static CSingleton* GetInstance() { static CSingleton* p = NULL; if( p == NULL ) p = new CSingleton(); return p; } }; unsigned __stdcall thread( void* ) { CSingleton* p = CSingleton::GetInstance(); p->DoSomeThing(); return 0; } int _tmain( int argc, _TCHAR* argv[] ) { for( int i = 0; i < 3; ++i ) { uintptr_t t = _beginthreadex( NULL, 0, thread, NULL, 0, NULL ); CloseHandle( (HANDLE)t ); } _getch(); return 0; }

上面的单件实现在单线程中肯定是正确的,不过在多线程中的输出却如下:

CSingleton::Constructor: Before Sleep CSingleton::Constructor: Before Sleep CSingleton::Constructor: Before Sleep CSingleton::Constructor: After Sleep CSingleton::DoSomeThing CSingleton::Constructor: After Sleep CSingleton::DoSomeThing CSingleton::Constructor: After Sleep CSingleton::DoSomeThing

很明显,虽然我们想做个单件,但它却出现了多个实例(或一个实例被初始化了多次)。其原因是我们的实现根本没有考虑多线程,那下面的代码把创建实例的部分锁住是不是就行了呢?

class CCriSec : CRITICAL_SECTION { public: CCriSec() { Sleep( 1000 ); // 增大出问题的概率, 但不改变逻辑 InitializeCriticalSection( this ); } ~CCriSec() { DeleteCriticalSection( this ); } void Enter() { EnterCriticalSection( this ); } void Leave() { LeaveCriticalSection( this ); } }; static CSingleton* GetInstance() { static CSingleton* p = NULL; static CCriSec lock; lock.Enter(); if( p == NULL ) p = new CSingleton(); lock.Leave(); return p; }

运行一下,不管输出是什么,程序崩溃了。分析一下可以发现,这个例子中的我们确实控制好了对 CSingleton 实例的初始化,但这种控制却依赖于另一个静态变量(CCriSec 的实例)的初始化,而这个新的静态变量导致了程序的崩溃,也就是说我们在解决问题的同时引入了新的问题。而且,在这种情况下,就算再引入多少个新的临界区也无济于事,因为对最外层的临界区的初始化总会有问题。

上面的例子的问题在于 CCriSec 是一种复杂的数据类型,所以对它的初始化总要到运行时才能完成,如果用整数这样简单的、能在编译期完成初始化的数据类型来做是不是可以呢?

static CSingleton* GetInstance() { static CSingleton* p = NULL; static volatile long lock = 0; if( InterlockedCompareExchange( &lock, 1, 0 ) == 0 ) p = new CSingleton(); return p; }

看起来好像没有问题,但运行一下却是下面的输出:

CSingleton::Constructor: Before Sleep CSingleton::DoSomeThing CSingleton::DoSomeThing CSingleton::Constructor: After Sleep CSingleton::DoSomeThing

也就是说 DoSomeThing 在构造函数返回之前已经被调用了,这显然也是错误的。其原因是我们忽略了“对象的创建时需要时间的”,把这个问题也修正一下,就是最终的正确实现了:

static CSingleton* GetInstance() { static CSingleton* p = NULL; static volatile long lock = 0; if( InterlockedCompareExchange( &lock, 1, 0 ) != 0 ) { while( lock != 2 ) // 等待对象创建完成 Sleep( 0 ); return p; } p = new CSingleton(); lock = 2; return p; }

本文采用的单件实现是函数内的静态变量,如果你采用其它方式,也会有类似问题。其实在我看来,单件模式是一个看起来简单、做对了很难(上面演示的是多线程中的问题,在具体的实践中还会遇到很多其他问题)、同时又没有太多实用价值的东西。

另外,从 Windows Vista 开始,微软提供了一种多线程下对象初始化的方法,有兴趣的可以中搜一下“INITONCE”,个人认为 INITONCE 有点完美的过头了,真正好玩又有用的是与它同时出现的“条件变量(condition variable)”,后面会写一些与它相关的内容。

PS:从 VS2015 开始,VC 编译器开始保证函数内的静态变量会在使用前完成初始化,所以文中的一些例子行为会有所不同。

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3455 回帖 • 149 关注
  • C++

    C++ 是在 C 语言的基础上开发的一种通用编程语言,应用广泛。C++ 支持多种编程范式,面向对象编程、泛型编程和过程化编程。

    108 引用 • 153 回帖
  • 线程
    123 引用 • 111 回帖 • 3 关注
  • 技术

    到底什么才是技术呢?

    88 引用 • 179 回帖 • 4 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3201 引用 • 8216 回帖 • 3 关注
  • RabbitMQ

    RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持多种语言客户端,如:Python、Ruby、.NET、Java、C、PHP、ActionScript 等。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

    49 引用 • 60 回帖 • 349 关注
  • abitmean

    有点意思就行了

    35 关注
  • 博客

    记录并分享人生的经历。

    273 引用 • 2388 回帖
  • Latke

    Latke 是一款以 JSON 为主的 Java Web 框架。

    71 引用 • 535 回帖 • 829 关注
  • 互联网

    互联网(Internet),又称网际网络,或音译因特网、英特网。互联网始于 1969 年美国的阿帕网,是网络与网络之间所串连成的庞大网络,这些网络以一组通用的协议相连,形成逻辑上的单一巨大国际网络。

    98 引用 • 367 回帖
  • Ruby

    Ruby 是一种开源的面向对象程序设计的服务器端脚本语言,在 20 世纪 90 年代中期由日本的松本行弘(まつもとゆきひろ/Yukihiro Matsumoto)设计并开发。在 Ruby 社区,松本也被称为马茨(Matz)。

    7 引用 • 31 回帖 • 260 关注
  • GAE

    Google App Engine(GAE)是 Google 管理的数据中心中用于 WEB 应用程序的开发和托管的平台。2008 年 4 月 发布第一个测试版本。目前支持 Python、Java 和 Go 开发部署。全球已有数十万的开发者在其上开发了众多的应用。

    14 引用 • 42 回帖 • 820 关注
  • 智能合约

    智能合约(Smart contract)是一种旨在以信息化方式传播、验证或执行合同的计算机协议。智能合约允许在没有第三方的情况下进行可信交易,这些交易可追踪且不可逆转。智能合约概念于 1994 年由 Nick Szabo 首次提出。

    1 引用 • 11 回帖 • 3 关注
  • OnlyOffice
    4 引用 • 17 关注
  • 链书

    链书(Chainbook)是 B3log 开源社区提供的区块链纸质书交易平台,通过 B3T 实现共享激励与价值链。可将你的闲置书籍上架到链书,我们共同构建这个全新的交易平台,让闲置书籍继续发挥它的价值。

    链书社

    链书目前已经下线,也许以后还有计划重制上线。

    14 引用 • 257 回帖 • 1 关注
  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    291 引用 • 4495 回帖 • 661 关注
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    947 引用 • 1460 回帖 • 2 关注
  • Q&A

    提问之前请先看《提问的智慧》,好的问题比好的答案更有价值。

    9999 引用 • 45441 回帖 • 74 关注
  • ReactiveX

    ReactiveX 是一个专注于异步编程与控制可观察数据(或者事件)流的 API。它组合了观察者模式,迭代器模式和函数式编程的优秀思想。

    1 引用 • 2 回帖 • 182 关注
  • LeetCode

    LeetCode(力扣)是一个全球极客挚爱的高质量技术成长平台,想要学习和提升专业能力从这里开始,充足技术干货等你来啃,轻松拿下 Dream Offer!

    209 引用 • 72 回帖 • 1 关注
  • 倾城之链
    23 引用 • 66 回帖 • 170 关注
  • danl
    173 关注
  • Word
    13 引用 • 41 回帖
  • DNSPod

    DNSPod 建立于 2006 年 3 月份,是一款免费智能 DNS 产品。 DNSPod 可以为同时有电信、网通、教育网服务器的网站提供智能的解析,让电信用户访问电信的服务器,网通的用户访问网通的服务器,教育网的用户访问教育网的服务器,达到互联互通的效果。

    6 引用 • 26 回帖 • 539 关注
  • Oracle

    Oracle(甲骨文)公司,全称甲骨文股份有限公司(甲骨文软件系统有限公司),是全球最大的企业级软件公司,总部位于美国加利福尼亚州的红木滩。1989 年正式进入中国市场。2013 年,甲骨文已超越 IBM,成为继 Microsoft 后全球第二大软件公司。

    107 引用 • 127 回帖 • 341 关注
  • AWS
    11 引用 • 28 回帖 • 9 关注
  • 深度学习

    深度学习(Deep Learning)是机器学习的分支,是一种试图使用包含复杂结构或由多重非线性变换构成的多个处理层对数据进行高层抽象的算法。

    43 引用 • 44 回帖
  • SpaceVim

    SpaceVim 是一个社区驱动的模块化 vim/neovim 配置集合,以模块的方式组织管理插件以
    及相关配置,为不同的语言开发量身定制了相关的开发模块,该模块提供代码自动补全,
    语法检查、格式化、调试、REPL 等特性。用户仅需载入相关语言的模块即可得到一个开箱
    即用的 Vim-IDE。

    3 引用 • 31 回帖 • 110 关注
  • 音乐

    你听到信仰的声音了么?

    62 引用 • 512 回帖
  • SMTP

    SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。

    4 引用 • 18 回帖 • 630 关注
  • 电影

    这是一个不能说的秘密。

    122 引用 • 608 回帖