C++ call_once淺析

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;
}

在這裏,提供了:

  1. std::once_flag flag; : 針對std::call_once的標記。
  2. fun : 需要調用的函數。
  3. 函數的參數信息。

下面我們看下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();
	}

在這個函數中,做了如下的事情:

  1. 定義一個函數和參數的元組: _Tuple _Tup(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)..., _Exc);
  2. auto _Lambda = [](void *, void *_Pv, void **) : 定義一個lambda函數,lambda函數中根據_Invoke_once(*_Ptup, _Seq());調用我們指定的函數。
  3. _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;
}

這個函數分爲兩步走:

  1. 如果版本支持的話,調用kernel32的InitOnceExecuteOnce函數來完成。
  2. 否則使用原子鎖來同步函數的調用。

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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章