C++ call_once淺析
從C++11 開始,引入一個call_once
東西,這個函數的作用是提供一個只調用一次某個函數的功能。在現實開發中,這個東西還是比較重要的,例如單實例對象的創建。
本文我們來探討一下std::call_once
的基本原理。
1. 使用
std::call_once
的使用過程如下:
#include <iostream>
#include <functional>
#include <mutex>
int fun(int a, int b, int c, int d)
{
std::cout << a << std::endl;
std::cout << b << std::endl;
std::cout << c << std::endl;
std::cout << d << std::endl;
return 0;
}
int main()
{
std::once_flag flag;
std::call_once(flag, fun, 100, 200, 300, 400);
return 0;
}
在這裏,提供了:
std::once_flag flag;
: 針對std::call_once
的標記。fun
: 需要調用的函數。- 函數的參數信息。
下面我們看下std::call_once
的基本原理。
2. std::once_flag
在C++庫中,std::once_flag
的定義如下:
struct once_flag
{ // opaque data structure for call_once()
_CONST_FUN once_flag() _NOEXCEPT
: _Opaque(0)
{ // default construct
}
once_flag(const once_flag&) = delete;
once_flag& operator=(const once_flag&) = delete;
void *_Opaque;
};
std::once_flag
是一個類,定義了一個指針void *_Opaque;
;後續我們將看到標準庫怎麼樣使用這個標記。
3. std::call_once
這是一個模板函數,定義如下:
template<class _Fn,
class... _Args> inline
void (call_once)(once_flag& _Flag, _Fn&& _Fx, _Args&&... _Ax)
{ // call _Fx(_Ax...) once
typedef tuple<_Fn&&, _Args&&..., _XSTD exception_ptr&> _Tuple;
typedef make_integer_sequence<size_t, 1 + sizeof...(_Args)> _Seq;
_XSTD exception_ptr _Exc;
_Tuple _Tup(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)..., _Exc);
auto _Lambda = [](void *, void *_Pv, void **)
{ // adapt call_once() to callback API
_Tuple *_Ptup = static_cast<_Tuple *>(_Pv);
_TRY_BEGIN
_Invoke_once(*_Ptup, _Seq());
_CATCH_ALL
auto& _Ref = _STD get<1 + sizeof...(_Args)>(*_Ptup);
_Ref = _XSTD current_exception();
return (0);
_CATCH_END
return (1);
};
if (_Execute_once(_Flag, _Lambda, _STD addressof(_Tup)) != 0)
return;
if (_Exc)
_XSTD rethrow_exception(_Exc);
_XGetLastError();
}
在這個函數中,做了如下的事情:
- 定義一個函數和參數的元組:
_Tuple _Tup(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)..., _Exc);
auto _Lambda = [](void *, void *_Pv, void **)
: 定義一個lambda函數,lambda函數中根據_Invoke_once(*_Ptup, _Seq());
調用我們指定的函數。_Execute_once(_Flag, _Lambda, _STD addressof(_Tup))
: 調用lambda函數。
4. _Execute_once
_Execute_once
這個函數中會同步調用一個函數,我們看下這個函數的實現原理:
_CRTIMP2_PURE int __CLRCALL_PURE_OR_CDECL _Execute_once(
once_flag& _Flag, _Lambda_fp_t _Lambda_fp, void *_Pv) _NOEXCEPT
{ // wrap Win32 InitOnceExecuteOnce()
static_assert(sizeof(_Flag._Opaque) == sizeof(INIT_ONCE), "invalid size");
return (__crtInitOnceExecuteOnce(
reinterpret_cast<PINIT_ONCE>(&_Flag._Opaque),
reinterpret_cast<PINIT_ONCE_FN>(_Lambda_fp),
_Pv, 0));
}
在這裏,主要調用__crtInitOnceExecuteOnce
來完成同步調用,這個函數的實現如下:
int __cdecl __crtInitOnceExecuteOnce(_RTL_RUN_ONCE *InitOnce, int (__stdcall *InitFn)(_RTL_RUN_ONCE *, void *, void **), void *Parameter, void **Context)
{
int result; // eax@2
int ret; // [sp+Ch] [bp-1Ch]@6
signed __int32 next; // [sp+14h] [bp-14h]@6
int pfInitOnceExecuteOnce; // [sp+20h] [bp-8h]@1
void *const previous; // [sp+24h] [bp-4h]@3
pfInitOnceExecuteOnce = __security_cookie ^ *(&__encodedKERNEL32Functions + 5);
if ( pfInitOnceExecuteOnce )
{
_guard_check_icall(pfInitOnceExecuteOnce);
result = ((int (__stdcall *)(_RTL_RUN_ONCE *, int (__stdcall *)(_RTL_RUN_ONCE *, void *, void **), void *, void **))pfInitOnceExecuteOnce)(
InitOnce,
InitFn,
Parameter,
Context);
}
else
{
while ( 1 )
{
previous = (void *const )_InterlockedCompareExchange((volatile signed __int32 *)InitOnce, 1, 0);
if ( previous == (void *const )2 )
return 1;
if ( !previous )
break;
if ( previous != (void *const )1 )
{
SetLastError(0xDu);
return 0;
}
Sleep(0);
}
next = 2;
ret = 1;
_guard_check_icall((unsigned int)InitFn);
if ( !InitFn(InitOnce, Parameter, Context) )
{
next = 0;
ret = 0;
}
if ( _InterlockedExchange((volatile signed __int32 *)InitOnce, next) == 1 )
{
result = ret;
}
else
{
SetLastError(0xDu);
result = 0;
}
}
return result;
}
這個函數分爲兩步走:
- 如果版本支持的話,調用kernel32的
InitOnceExecuteOnce
函數來完成。 - 否則使用原子鎖來同步函數的調用。
5. CCallOnce的實現
這裏我們模擬上面這個過程,實現CCallOnce
。
#include <windows.h>
#include <iostream>
#include <functional>
#include <mutex>
int fun(int a, int b, int c, int d)
{
std::cout << a << std::endl;
std::cout << b << std::endl;
std::cout << c << std::endl;
std::cout << d << std::endl;
return 0;
}
class CCallOnce
{
private:
enum OFLAGS
{
FLAG_NOT_CALL = 0,
FLAG_CALLING = 1,
FLAG_CALL_OVER = 2,
FLAG_INVALID = -1
};
public:
CCallOnce() : OnceFlag(FLAG_NOT_CALL){
}
~CCallOnce()
{
}
template<typename Fun, typename... types>
void CallOnce(const Fun& function, types&& ... args)
{
while (true)
{
ULONG previous = InterlockedCompareExchange(&OnceFlag, FLAG_CALLING, FLAG_NOT_CALL);
if (previous == FLAG_CALL_OVER)
return;
if (previous == FLAG_NOT_CALL)
break;
if (previous != FLAG_CALLING)
{
SetLastError(0xDu);
return;
}
Sleep(0);
}
ULONG next = FLAG_CALL_OVER;
function(std::forward<types>(args)...);
if (InterlockedExchange(&OnceFlag, next) == FLAG_CALLING)
{
//ok
}
else
{
SetLastError(0xDu);
//error
}
}
private:
volatile ULONG OnceFlag;
};
int main()
{
CCallOnce once;
once.CallOnce(fun, 100, 200, 300, 400);
once.CallOnce(fun, 100, 200, 300, 400);
return 0;
}