getter 的 c++ 临界区
c++ criticalsection for getter
我有一个简单的 class,其中包含一个私有成员,可在多线程环境(多个 readers/multi 编写器)中通过 get() 和 set() 进行访问。我如何锁定 Get() 因为它只有一个 return 语句?
class MyValue
{
private:
System::CriticalSection lock;
int val { 0 };
public:
int SetValue(int arg)
{
lock.Enter();
val = arg;
lock.Leave();
}
int GetValue()
{
lock.Enter();
return val;
//Where should I do lock.Leave()?
}
}
考虑使用 class 包装器在 ctor 中锁定,并在 dtor 中解锁。查看标准实现:http://en.cppreference.com/w/cpp/thread/unique_lock
这样您就不需要记住解锁以防复杂代码或代码中抛出异常,从而改变正常执行。
我不是多线程专家,但我认为以下内容应该可行。
int GetValue()
{
lock.Enter();
int ret = val;
lock.Leave();
return ret;
}
不要锁定任何东西。在你的例子中,如果你让你的成员成为一个 std::atomic
整数就足够了。
这里不需要其他任何东西。事实上,由于 Intel 架构(强内存排序模型),这 std::atomic
甚至不太可能导致任何性能问题。
这是 hauron 回答中同步对象的演示——我想表明对象构造和销毁开销根本不存在于优化构建中。
在下面的代码中,CCsGrabber 是一个类似 RAII 的 class,它在构造时进入临界区(由 CCritical 对象包装),然后在销毁时离开它:
class CCsGrabber {
class CCritical& m_Cs;
CCsGrabber();
public:
CCsGrabber(CCritical& cs);
~CCsGrabber();
};
class CCritical {
CRITICAL_SECTION cs;
public:
CCritical() {
InitializeCriticalSection(&cs);
}
~CCritical() { DeleteCriticalSection(&cs); }
void Enter() { EnterCriticalSection(&cs); }
void Leave() { LeaveCriticalSection(&cs); }
void Lock() { Enter(); }
void Unlock() { Leave(); }
};
inline CCsGrabber::CCsGrabber(CCritical& cs) : m_Cs(cs) { m_Cs.Enter(); }
inline CCsGrabber::CCsGrabber(CCritical *pcs) : m_Cs(*pcs) { m_Cs.Enter(); }
inline CCsGrabber::~CCsGrabber() { m_Cs.Leave(); }
现在,创建了一个全局 CCritical 对象 (cs),它在 SerialFunc()
中与本地 CCsGrabber 实例 (csg) 一起用于处理锁定和解锁:
CCritical cs;
DWORD last_tick = 0;
void SerialFunc() {
CCsGrabber csg(cs);
last_tick = GetTickCount();
}
int main() {
SerialFunc();
std::cout << last_tick << std::endl;
}
下面是来自优化的 32 位构建的 main() 的反汇编。 (我为粘贴整个内容而道歉——我想表明我没有隐藏任何东西:
int main() {
00401C80 push ebp
00401C81 mov ebp,esp
00401C83 and esp,0FFFFFFF8h
00401C86 push 0FFFFFFFFh
00401C88 push 41B038h
00401C8D mov eax,dword ptr fs:[00000000h]
00401C93 push eax
00401C94 mov dword ptr fs:[0],esp
00401C9B sub esp,0Ch
00401C9E push esi
00401C9F push edi
SerialFunc();
00401CA0 push 427B78h ; pointer to CS object
00401CA5 call dword ptr ds:[41C00Ch] ; _RtlEnterCriticalSection@4:
00401CAB call dword ptr ds:[41C000h] ; _GetTickCountStub@0:
00401CB1 push 427B78h ; pointer to CS object
00401CB6 mov dword ptr ds:[00427B74h],eax ; return value => last_tick
00401CBB call dword ptr ds:[41C008h] ; _RtlLeaveCriticalSection@4:
std::cout << last_tick << std::endl;
00401CC1 push ecx
00401CC2 call std::basic_ostream<char,std::char_traits<char> >::operator<< (0401D90h)
00401CC7 mov esi,eax
00401CC9 lea eax,[esp+0Ch]
00401CCD push eax
00401CCE mov ecx,dword ptr [esi]
00401CD0 mov ecx,dword ptr [ecx+4]
00401CD3 add ecx,esi
00401CD5 call std::ios_base::getloc (0401BD0h)
00401CDA push eax
00401CDB mov dword ptr [esp+20h],0
00401CE3 call std::use_facet<std::ctype<char> > (0403E40h)
00401CE8 mov dword ptr [esp+20h],0FFFFFFFFh
00401CF0 add esp,4
00401CF3 mov ecx,dword ptr [esp+0Ch]
00401CF7 mov edi,eax
00401CF9 test ecx,ecx
00401CFB je main+8Eh (0401D0Eh)
00401CFD mov edx,dword ptr [ecx]
00401CFF call dword ptr [edx+8]
00401D02 test eax,eax
00401D04 je main+8Eh (0401D0Eh)
00401D06 mov edx,dword ptr [eax]
00401D08 mov ecx,eax
00401D0A push 1
00401D0C call dword ptr [edx]
00401D0E mov eax,dword ptr [edi]
00401D10 mov ecx,edi
00401D12 push 0Ah
00401D14 mov eax,dword ptr [eax+20h]
00401D17 call eax
00401D19 movzx eax,al
00401D1C mov ecx,esi
00401D1E push eax
00401D1F call std::basic_ostream<char,std::char_traits<char> >::put (0404220h)
00401D24 mov ecx,esi
00401D26 call std::basic_ostream<char,std::char_traits<char> >::flush (0402EB0h)
}
00401D2B mov ecx,dword ptr [esp+14h]
00401D2F xor eax,eax
00401D31 pop edi
00401D32 mov dword ptr fs:[0],ecx
00401D39 pop esi
00401D3A mov esp,ebp
00401D3C pop ebp
00401D3D ret
所以我们可以看到 SerialFunc() 被直接内联到 main 中,在开头的序言之后和 cout 代码之前 -- 没有任何多余的对象创建、内存分配或任何东西 -- 它只是看起来像进入临界区所需的最少汇编代码量,获取变量中的滴答计数,然后离开临界区。
然后我把SerialFunc()
改成了:
void SerialFunc() {
cs.Enter();
last_tick = GetTickCount();
cs.Leave();
}
显式放置 cs.Enter()
和 cs.Leave()
,只是为了与 RAII 版本进行比较。生成的代码原来是相同的:
int main() {
00401C80 push ebp
00401C81 mov ebp,esp
00401C83 and esp,0FFFFFFF8h
00401C86 push 0FFFFFFFFh
00401C88 push 41B038h
00401C8D mov eax,dword ptr fs:[00000000h]
00401C93 push eax
00401C94 mov dword ptr fs:[0],esp
00401C9B sub esp,0Ch
00401C9E push esi
00401C9F push edi
SerialFunc();
00401CA0 push 427B78h
00401CA5 call dword ptr ds:[41C00Ch]
00401CAB call dword ptr ds:[41C000h]
00401CB1 push 427B78h
00401CB6 mov dword ptr ds:[00427B74h],eax
00401CBB call dword ptr ds:[41C008h]
std::cout << last_tick << std::endl;
00401CC1 push ecx
00401CC2 call std::basic_ostream<char,std::char_traits<char> >::operator<< (0401D90h)
...
在我看来,SergeyA 的答案最适合给定的情况——用于同步读写的关键部分 from/to 32 位变量过多。但是,如果出现需要关键部分或互斥锁的情况,使用类似 RAII 的对象来简化您的代码可能不会产生重大(甚至任何)对象创建开销。
(以上代码我是用Visual C++ 2013编译的)
我有一个简单的 class,其中包含一个私有成员,可在多线程环境(多个 readers/multi 编写器)中通过 get() 和 set() 进行访问。我如何锁定 Get() 因为它只有一个 return 语句?
class MyValue
{
private:
System::CriticalSection lock;
int val { 0 };
public:
int SetValue(int arg)
{
lock.Enter();
val = arg;
lock.Leave();
}
int GetValue()
{
lock.Enter();
return val;
//Where should I do lock.Leave()?
}
}
考虑使用 class 包装器在 ctor 中锁定,并在 dtor 中解锁。查看标准实现:http://en.cppreference.com/w/cpp/thread/unique_lock
这样您就不需要记住解锁以防复杂代码或代码中抛出异常,从而改变正常执行。
我不是多线程专家,但我认为以下内容应该可行。
int GetValue()
{
lock.Enter();
int ret = val;
lock.Leave();
return ret;
}
不要锁定任何东西。在你的例子中,如果你让你的成员成为一个 std::atomic
整数就足够了。
这里不需要其他任何东西。事实上,由于 Intel 架构(强内存排序模型),这 std::atomic
甚至不太可能导致任何性能问题。
这是 hauron 回答中同步对象的演示——我想表明对象构造和销毁开销根本不存在于优化构建中。
在下面的代码中,CCsGrabber 是一个类似 RAII 的 class,它在构造时进入临界区(由 CCritical 对象包装),然后在销毁时离开它:
class CCsGrabber {
class CCritical& m_Cs;
CCsGrabber();
public:
CCsGrabber(CCritical& cs);
~CCsGrabber();
};
class CCritical {
CRITICAL_SECTION cs;
public:
CCritical() {
InitializeCriticalSection(&cs);
}
~CCritical() { DeleteCriticalSection(&cs); }
void Enter() { EnterCriticalSection(&cs); }
void Leave() { LeaveCriticalSection(&cs); }
void Lock() { Enter(); }
void Unlock() { Leave(); }
};
inline CCsGrabber::CCsGrabber(CCritical& cs) : m_Cs(cs) { m_Cs.Enter(); }
inline CCsGrabber::CCsGrabber(CCritical *pcs) : m_Cs(*pcs) { m_Cs.Enter(); }
inline CCsGrabber::~CCsGrabber() { m_Cs.Leave(); }
现在,创建了一个全局 CCritical 对象 (cs),它在 SerialFunc()
中与本地 CCsGrabber 实例 (csg) 一起用于处理锁定和解锁:
CCritical cs;
DWORD last_tick = 0;
void SerialFunc() {
CCsGrabber csg(cs);
last_tick = GetTickCount();
}
int main() {
SerialFunc();
std::cout << last_tick << std::endl;
}
下面是来自优化的 32 位构建的 main() 的反汇编。 (我为粘贴整个内容而道歉——我想表明我没有隐藏任何东西:
int main() {
00401C80 push ebp
00401C81 mov ebp,esp
00401C83 and esp,0FFFFFFF8h
00401C86 push 0FFFFFFFFh
00401C88 push 41B038h
00401C8D mov eax,dword ptr fs:[00000000h]
00401C93 push eax
00401C94 mov dword ptr fs:[0],esp
00401C9B sub esp,0Ch
00401C9E push esi
00401C9F push edi
SerialFunc();
00401CA0 push 427B78h ; pointer to CS object
00401CA5 call dword ptr ds:[41C00Ch] ; _RtlEnterCriticalSection@4:
00401CAB call dword ptr ds:[41C000h] ; _GetTickCountStub@0:
00401CB1 push 427B78h ; pointer to CS object
00401CB6 mov dword ptr ds:[00427B74h],eax ; return value => last_tick
00401CBB call dword ptr ds:[41C008h] ; _RtlLeaveCriticalSection@4:
std::cout << last_tick << std::endl;
00401CC1 push ecx
00401CC2 call std::basic_ostream<char,std::char_traits<char> >::operator<< (0401D90h)
00401CC7 mov esi,eax
00401CC9 lea eax,[esp+0Ch]
00401CCD push eax
00401CCE mov ecx,dword ptr [esi]
00401CD0 mov ecx,dword ptr [ecx+4]
00401CD3 add ecx,esi
00401CD5 call std::ios_base::getloc (0401BD0h)
00401CDA push eax
00401CDB mov dword ptr [esp+20h],0
00401CE3 call std::use_facet<std::ctype<char> > (0403E40h)
00401CE8 mov dword ptr [esp+20h],0FFFFFFFFh
00401CF0 add esp,4
00401CF3 mov ecx,dword ptr [esp+0Ch]
00401CF7 mov edi,eax
00401CF9 test ecx,ecx
00401CFB je main+8Eh (0401D0Eh)
00401CFD mov edx,dword ptr [ecx]
00401CFF call dword ptr [edx+8]
00401D02 test eax,eax
00401D04 je main+8Eh (0401D0Eh)
00401D06 mov edx,dword ptr [eax]
00401D08 mov ecx,eax
00401D0A push 1
00401D0C call dword ptr [edx]
00401D0E mov eax,dword ptr [edi]
00401D10 mov ecx,edi
00401D12 push 0Ah
00401D14 mov eax,dword ptr [eax+20h]
00401D17 call eax
00401D19 movzx eax,al
00401D1C mov ecx,esi
00401D1E push eax
00401D1F call std::basic_ostream<char,std::char_traits<char> >::put (0404220h)
00401D24 mov ecx,esi
00401D26 call std::basic_ostream<char,std::char_traits<char> >::flush (0402EB0h)
}
00401D2B mov ecx,dword ptr [esp+14h]
00401D2F xor eax,eax
00401D31 pop edi
00401D32 mov dword ptr fs:[0],ecx
00401D39 pop esi
00401D3A mov esp,ebp
00401D3C pop ebp
00401D3D ret
所以我们可以看到 SerialFunc() 被直接内联到 main 中,在开头的序言之后和 cout 代码之前 -- 没有任何多余的对象创建、内存分配或任何东西 -- 它只是看起来像进入临界区所需的最少汇编代码量,获取变量中的滴答计数,然后离开临界区。
然后我把SerialFunc()
改成了:
void SerialFunc() {
cs.Enter();
last_tick = GetTickCount();
cs.Leave();
}
显式放置 cs.Enter()
和 cs.Leave()
,只是为了与 RAII 版本进行比较。生成的代码原来是相同的:
int main() {
00401C80 push ebp
00401C81 mov ebp,esp
00401C83 and esp,0FFFFFFF8h
00401C86 push 0FFFFFFFFh
00401C88 push 41B038h
00401C8D mov eax,dword ptr fs:[00000000h]
00401C93 push eax
00401C94 mov dword ptr fs:[0],esp
00401C9B sub esp,0Ch
00401C9E push esi
00401C9F push edi
SerialFunc();
00401CA0 push 427B78h
00401CA5 call dword ptr ds:[41C00Ch]
00401CAB call dword ptr ds:[41C000h]
00401CB1 push 427B78h
00401CB6 mov dword ptr ds:[00427B74h],eax
00401CBB call dword ptr ds:[41C008h]
std::cout << last_tick << std::endl;
00401CC1 push ecx
00401CC2 call std::basic_ostream<char,std::char_traits<char> >::operator<< (0401D90h)
...
在我看来,SergeyA 的答案最适合给定的情况——用于同步读写的关键部分 from/to 32 位变量过多。但是,如果出现需要关键部分或互斥锁的情况,使用类似 RAII 的对象来简化您的代码可能不会产生重大(甚至任何)对象创建开销。
(以上代码我是用Visual C++ 2013编译的)