在下面的程序中,我尝试print使用函数本地互斥对象使函数成为线程安全的:
#include <iostream>
#include <chrono>
#include <mutex>
#include <string>
#include <thread>
void print(const std::string & s)
{
// Thread safe?
static std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
std::cout <<s << std::endl;
}
int main()
{
std::thread([&](){ for (int i = 0; i < 10; ++i) print("a" + std::to_string(i)); }).detach();
std::thread([&](){ for (int i = 0; i < 10; ++i) print("b" + std::to_string(i)); }).detach();
std::thread([&](){ for (int i = 0; i < 10; ++i) print("c" + std::to_string(i)); }).detach();
std::thread([&](){ for (int i = 0; i < 10; ++i) print("d" + std::to_string(i)); }).detach();
std::thread([&](){ for (int i = 0; i < 10; ++i) print("e" + std::to_string(i)); }).detach();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
Run Code Online (Sandbox Code Playgroud)
这样安全吗?
Bee*_*ope 19
在C++ 11及更高版本中,函数本地静态变量的初始化是线程安全的,因此保证上面的代码是安全的.
这种方式在实践中的作用是编译器在函数本身中插入任何必要的样板,以检查变量是否在访问之前被初始化.在的情况下,std::mutex为实现中gcc,clang并且icc然而,在初始化状态是全零,所以不需要明确的初始化(变量将生活在全零.bss部分,因此,初始化是"免费"),因为我们看到从组装1:
inc(int& i):
mov eax, OFFSET FLAT:_ZL28__gthrw___pthread_key_createPjPFvPvE
test rax, rax
je .L2
push rbx
mov rbx, rdi
mov edi, OFFSET FLAT:_ZZ3incRiE3mtx
call _ZL26__gthrw_pthread_mutex_lockP15pthread_mutex_t
test eax, eax
jne .L10
add DWORD PTR [rbx], 1
mov edi, OFFSET FLAT:_ZZ3incRiE3mtx
pop rbx
jmp _ZL28__gthrw_pthread_mutex_unlockP15pthread_mutex_t
.L2:
add DWORD PTR [rdi], 1
ret
.L10:
mov edi, eax
call _ZSt20__throw_system_errori
Run Code Online (Sandbox Code Playgroud)
请注意,从该行开始,mov edi, OFFSET FLAT:_ZZ3incRiE3mtx它只是加载inc::mtx函数本地静态的地址并调用pthread_mutex_lock它,而不进行任何初始化.处理之前的代码pthread_key_create显然只是检查pthreads库是否存在.
但是,并不能保证所有实现都将实现std::mutex为全零,因此在某些情况下,您可能会在每次调用时产生持续开销,以检查是否mutex已初始化.在函数外声明互斥锁可以避免这种情况.
下面是一个示例,将两种方法与mutex2带有非可嵌入构造函数的替换类进行对比(因此编译器无法确定初始状态是全零):
#include <mutex>
class mutex2 {
public:
mutex2();
void lock();
void unlock();
};
void inc_local(int &i)
{
// Thread safe?
static mutex2 mtx;
std::unique_lock<mutex2> lock(mtx);
i++;
}
mutex2 g_mtx;
void inc_global(int &i)
{
std::unique_lock<mutex2> lock(g_mtx);
i++;
}
Run Code Online (Sandbox Code Playgroud)
函数本地版本编译(打开gcc)到:
inc_local(int& i):
push rbx
movzx eax, BYTE PTR _ZGVZ9inc_localRiE3mtx[rip]
mov rbx, rdi
test al, al
jne .L3
mov edi, OFFSET FLAT:_ZGVZ9inc_localRiE3mtx
call __cxa_guard_acquire
test eax, eax
jne .L12
.L3:
mov edi, OFFSET FLAT:_ZZ9inc_localRiE3mtx
call _ZN6mutex24lockEv
add DWORD PTR [rbx], 1
mov edi, OFFSET FLAT:_ZZ9inc_localRiE3mtx
pop rbx
jmp _ZN6mutex26unlockEv
.L12:
mov edi, OFFSET FLAT:_ZZ9inc_localRiE3mtx
call _ZN6mutex2C1Ev
mov edi, OFFSET FLAT:_ZGVZ9inc_localRiE3mtx
call __cxa_guard_release
jmp .L3
mov rbx, rax
mov edi, OFFSET FLAT:_ZGVZ9inc_localRiE3mtx
call __cxa_guard_abort
mov rdi, rbx
call _Unwind_Resume
Run Code Online (Sandbox Code Playgroud)
注意大量的样板处理__cxa_guard_*函数.首先,检查rip-relative标志字节_ZGVZ9inc_localRiE3mtx2,如果非零,则变量已经初始化,我们完成并进入快速路径.不需要原子操作,因为在x86上,加载已经具有所需的获取语义.
如果此检查失败,我们将转到慢速路径,这实际上是一种双重检查锁定形式:初始检查不足以确定变量需要初始化,因为两个或多个线程可能在这里竞争.该__cxa_guard_acquire调用执行锁定和第二次检查,并且也可以通过快速路径(如果另一个线程同时初始化对象),或者可以跳转到实际的初始化代码.L12.
最后请注意,程序集中的最后5条指令根本无法从函数直接到达,因为它们之前是无条件的,jmp .L3并且没有任何跳转到它们.如果对构造函数的调用mutex2()在某个时刻抛出异常,它们就会被异常处理程序跳转到那里.
总的来说,我们可以说第一次访问初始化的运行时成本是低到中等,因为快速路径只检查单个字节标志而没有任何昂贵的指令(并且函数本身的其余部分通常意味着至少两个原子操作mutex.lock()而且mutex.unlock(),它会增加代码量.
与全局版本相比,除了在全局初始化期间而不是在首次访问之前发生初始化之外,它是相同的:
inc_global(int& i):
push rbx
mov rbx, rdi
mov edi, OFFSET FLAT:g_mtx
call _ZN6mutex24lockEv
add DWORD PTR [rbx], 1
mov edi, OFFSET FLAT:g_mtx
pop rbx
jmp _ZN6mutex26unlockEv
Run Code Online (Sandbox Code Playgroud)
该函数不到三分之一大小,根本没有任何初始化样板.
但是,在C++ 11之前,这通常是不安全的,除非您的编译器对静态本地的初始化方式做出了一些特殊的保证.
前段时间,在查看类似问题时,我检查了Visual Studio为此案例生成的程序集.为您的print方法生成的汇编代码的伪代码看起来像这样:
void print(const std::string & s)
{
if (!init_check_print_mtx) {
init_check_print_mtx = true;
mtx.mutex(); // call mutex() ctor for mtx
}
// ... rest of method
}
Run Code Online (Sandbox Code Playgroud)
这init_check_print_mtx是一个特定于此方法的编译器生成的全局变量,用于跟踪是否已初始化本地静态.请注意,在此变量保护的"一次"初始化块内,在初始化互斥锁之前将该变量设置为true.
不过,我觉得这是愚蠢的,因为它保证了其他线程竞相进入这一方法将跳过初始化,并使用未初始化mtx-对的可能初始化替代mtx一次以上-但实际上做这种方式可以让你避免了无限递归问题是如果std::mutex()要回调打印,则会出现这种情况,而且这种行为实际上是由标准强制执行的.
上面的Nemo提到在C++ 11中已经修复了(更确切地说,重新指定)这需要等待所有的竞争线程,这会使这个安全,但是你需要检查你自己的编译器是否合规.我没有检查实际上新的规范是否包含这个保证,但是我不会感到惊讶,因为在没有这个的情况下,局部静态在多线程环境中几乎没用(除非原始值没有任何检查和设置行为,因为它们只是直接引用.data段中已经初始化的位置).
1请注意,我将print()函数更改为稍微简单的inc()函数,该函数仅增加锁定区域中的整数.它具有与原始相同的锁定结构和含义,但避免了一堆处理<<运算符的代码std::cout.
2使用c++filt这个de-mangles来guard variable for inc_local(int&)::mtx.
Jon*_*ely 16
由于几个原因,这与链接的问题不同.
链接的问题不是C++ 11,而是你的.在C++ 11中,函数局部静态变量的初始化始终是安全的.在C++ 11之前,只有一些编译器安全,例如GCC和Clang默认为线程安全初始化.
链接的问题通过调用函数初始化引用,该函数是动态初始化并在运行时发生.默认构造函数std::mutex是constexpr因为您的静态变量具有常量初始化,即可以在编译时(或链接时)初始化互斥锁,因此在运行时无需动态执行任何操作.即使多个线程同时调用该函数,在使用互斥锁之前他们实际上也不需要做任何事情.
您的代码是安全的(假设您的编译器正确实现了C++ 11规则.)
只要互斥锁是静态的,是的.
本地的,非静止的,绝对不安全.除非你所有的线程使用相同的堆栈,这也意味着你现在已经发明了一个单元可以同时拥有许多不同值的存储器,并且只是等待诺贝尔委员会通知你下一个诺贝尔奖.
您必须为互斥锁提供某种"全局"(共享)内存空间.
| 归档时间: |
|
| 查看次数: |
9786 次 |
| 最近记录: |