一、单次调用
开发者很容易混淆单次调用和单实例两种机制,可能觉得二者没有区别。在前面的分析中,对单实例也就是唯一对象的处理进行过实现分析,而且其中的实施也使用了单次调用的方法。单次调用不仅可以用在生成单实例上,也可以用在普通的多实例上。
单次调用的价值在于对外不会产生副作用,无论从形式上或内容上,都保持了正常开发的调用机制,但达到了只进行唯一一次调用的过程。单次调用的应用场景非常丰富,比如常见的单实例、多线程的安全调用以及在某些特殊平台上的接口单一控制(如硬件的初始化)等等。
二、为什么需要单次调用
C++中处理单次调用的目的主要有以下几个:
- 减少重复调用,提高效率
- 防止多线程出现多次调用产生竞态,导致状态的不稳定,进而引发程序的异常
- 重复初始化导致的配置异常,导致运行结果的问题
- 硬件平台不支持重复初始化可能导致软、硬件异常
- 设计模式如单实例的需要
- 单次调用某些情况下支持延迟加载,提高加载速度
- 架构设计或功能的需要
三、C++的应对方法
在C++中要想实现单次调用的方式,其实非常简单。当然,这是指在较新的C++标准情况下。一般来说,推荐使用的方法主要有:
- std::call_once
这是推荐的使用方法,通过std::call_once和std::once_flag配合,可以达到对接口的单次调用的目的(懒汉模式)。它可以保证在多线程调用的情况下,接口只被调用一次。这也是创建单实例的一重要方式。它的优势在于全方位的操作能力,容易维护,但对标准要求稍高(C++11及以后)。 - 显式的全局调用
这种情况一般只在程序启动时,调用一次接口,以后则不再允许调用(饿汉模式)。这种情况涉及不到多线程也涉及不到复杂的调用,操作简单,便于维护管理。但缺点也比较明显,无法防止开发者重复、忽略调用。这更像是一种业务逻辑的控制而非技术层面的控制。 - 局部或inline静态调用
局部静态调用这个在C++11后已经由编译器保证了多线程的安全性,在C++17后的inline静态变量也提供了类似的机制。区别在于,局部静态变量标准是保证其初始化的多线程安全的,而inline静态变量则只保证全局的唯一性而无法保证多线程的安全初始化。也就是说,它更适合于显式的全局调用中使用。
这类机制的缺点在于无法进行顺利的动态沟通,包括初始化出现问题的异常处理都存在着风险。 - 使用同步操作
包括使用互斥锁或使用原子锁以及类似智能指针的引用计数器控制都可以实现单次调用的目的,但操作复杂,效率较低,还容易出现各种细节的处理问题。
四、实例
根据上面的情况给出几个例程,非常简单:
//call once#include<mutex>#include<thread>std::once_flag flag_;voidoneInit(){// 只执行一次std::cout<<"call once!"<<std::endl;}voidthreadFunc(){std::call_once(flag_,oneInit);}//局部静态变量Demo*callOnceOp(){//处理returnpDemo;}voidoneStaticFunc(){staticDemo*p=callOnceOp();}//inline静态class CallOnceDemo{public:staticinlineCallOnceDemo&ins=[]()->CallOnceDemo&{staticCallOnceDemo obj;returnobj;}();};重点是把代码与分析对应,而不是简单的看这些简单的代码。
五、总结
为什么强调要把基础打扎实?因为这是为后来将多种技术点融合的一个重要前提。正如战场技击,拳打还是脚踢其具体到每个动作招式都是一样的,但每次的对手和环境不同,那么出手的先后顺序和应对方式都有所不同。这就需要每个人能灵活的判断当前的具体情况,到底采用哪种技击的招式。僵化的使用招式结果就只能是挨打。明白这个道理也就明白了开发中为什么每个功能的实现后面会跟着多种实现的机制,而每种实现机制又不只是单纯为这种功能服务的原因了。