函数体中静态定义的互斥量是否能够正确锁定?
Is a mutex defined statically in a function body able to lock properly?
在函数体中静态定义的互斥量是否能够正确锁定?我目前在我的记录器系统中使用这个模式,但我还没有测试它的线程安全性。
void foo () {
static std::mutex mu;
std::lock_guard<std::mutex> guard(mu);
...
}
是的,这很好。第一次调用函数时 mu
将被初始化(这保证是线程安全的并且只发生一次)然后 guard
将锁定它。如果另一个线程调用 foo 它将等待
std::lock_guard<std::mutex> guard(mu);
直到第一次调用 foo
完成并且 guard
被销毁并解锁 mu
。
NathanOliver 的回答不准确:mu
实际上是 静态 初始化 – 这意味着在任何动态初始化之前,因此也在任何用户代码可以调用 mu.lock()
(直接或使用 std::lock_guard<std::mutex>
)。
尽管如此,您的用例是安全的 – 事实上,std::mutex
初始化甚至比之前答案建议的更安全。
这样做的原因是任何具有静态存储持续时间(✓检查)的变量都是用常量表达式初始化的(其中对 constexpr
构造函数的调用被显式地视为如此 – ✓检查),是 constant initialized,它是 static initialized 的子集。所有静态初始化严格发生在所有动态初始化之前,因此在您的函数可以第一次被调用之前发生。 (basic.start.static/2)
这适用于std::mutex
,因为std::mutex
只有一个可行的构造函数,默认构造函数,它被指定为constexpr
。 (thread.mutex.class)
因此,除了 C++11 及更高版本的常规原子性保证之外,动态初始化 函数范围内的静态变量,其他 std::mutex
个实例静态存储也完全不受初始化顺序问题的影响,例如:
#include <mutex>
extern std::mutex mtx;
unsigned counter = 0u;
const auto count = []{ std::lock_guard<std::mutex> lock{mtx}; return ++counter; };
const auto x = count(), y = count();
std::mutex mtx;
如果 mtx
是动态初始化的,此代码将表现出未定义的行为,因为 mtx
的初始化程序将 运行 在动态初始化的 x
和 y
,因此 mtx
将在初始化之前使用。
(在 pthread 或使用 pthread 的 <thread>
的常见实现中,此效果是通过使用常量表达式 PTHREAD_MUTEX_INITIALIZER
实现的。)
PS:对于std::atomic<T>
的实例也是如此,只要传递给构造函数的参数是常量表达式即可。这意味着例如您可以轻松地基于 std::atomic<IntT>
创建一个不受初始化顺序问题影响的自旋锁。 std::once_flag
具有相同的理想 属性。具有静态存储持续时间的 std::atomic_flag
也可以通过以下两种方式之一进行静态初始化:
std::atomic_flag f;
是零初始化的(因为静态存储持续时间和因为它有一个微不足道的默认 c'tor)。请注意,标志的状态仍然未指定,这使得这种方法相当无用。
std::atomic_flag f = ATOMIC_FLAG_INIT;
是常量初始化和取消设置。这就是您实际想要使用的。
在函数体中静态定义的互斥量是否能够正确锁定?我目前在我的记录器系统中使用这个模式,但我还没有测试它的线程安全性。
void foo () {
static std::mutex mu;
std::lock_guard<std::mutex> guard(mu);
...
}
是的,这很好。第一次调用函数时 mu
将被初始化(这保证是线程安全的并且只发生一次)然后 guard
将锁定它。如果另一个线程调用 foo 它将等待
std::lock_guard<std::mutex> guard(mu);
直到第一次调用 foo
完成并且 guard
被销毁并解锁 mu
。
NathanOliver 的回答不准确:mu
实际上是 静态 初始化 – 这意味着在任何动态初始化之前,因此也在任何用户代码可以调用 mu.lock()
(直接或使用 std::lock_guard<std::mutex>
)。
尽管如此,您的用例是安全的 – 事实上,std::mutex
初始化甚至比之前答案建议的更安全。
这样做的原因是任何具有静态存储持续时间(✓检查)的变量都是用常量表达式初始化的(其中对 constexpr
构造函数的调用被显式地视为如此 – ✓检查),是 constant initialized,它是 static initialized 的子集。所有静态初始化严格发生在所有动态初始化之前,因此在您的函数可以第一次被调用之前发生。 (basic.start.static/2)
这适用于std::mutex
,因为std::mutex
只有一个可行的构造函数,默认构造函数,它被指定为constexpr
。 (thread.mutex.class)
因此,除了 C++11 及更高版本的常规原子性保证之外,动态初始化 函数范围内的静态变量,其他 std::mutex
个实例静态存储也完全不受初始化顺序问题的影响,例如:
#include <mutex>
extern std::mutex mtx;
unsigned counter = 0u;
const auto count = []{ std::lock_guard<std::mutex> lock{mtx}; return ++counter; };
const auto x = count(), y = count();
std::mutex mtx;
如果 mtx
是动态初始化的,此代码将表现出未定义的行为,因为 mtx
的初始化程序将 运行 在动态初始化的 x
和 y
,因此 mtx
将在初始化之前使用。
(在 pthread 或使用 pthread 的 <thread>
的常见实现中,此效果是通过使用常量表达式 PTHREAD_MUTEX_INITIALIZER
实现的。)
PS:对于std::atomic<T>
的实例也是如此,只要传递给构造函数的参数是常量表达式即可。这意味着例如您可以轻松地基于 std::atomic<IntT>
创建一个不受初始化顺序问题影响的自旋锁。 std::once_flag
具有相同的理想 属性。具有静态存储持续时间的 std::atomic_flag
也可以通过以下两种方式之一进行静态初始化:
std::atomic_flag f;
是零初始化的(因为静态存储持续时间和因为它有一个微不足道的默认 c'tor)。请注意,标志的状态仍然未指定,这使得这种方法相当无用。std::atomic_flag f = ATOMIC_FLAG_INIT;
是常量初始化和取消设置。这就是您实际想要使用的。