![](/img/trans.png)
[英]Is it safe to mix boost::thread with C++11 std::mutex?
[英]Always declare std::mutex as mutable in C++11?
看了 Herb Sutter 的演讲你不知道 const 和 mutable ,我想知道我是否应该总是将互斥体定义为可变的? 如果是,我猜对于任何同步容器(例如, tbb::concurrent_queue
)也是如此?
一些背景:在他的演讲中,他指出 const == mutable == 线程安全,而std::mutex
是每个定义线程安全的。
也有关于谈话的相关问题, 在 C++11 中 const 是否意味着线程安全。
编辑:
在这里,我发现了一个相关的问题(可能是重复的)。 不过,在 C++11 之前就有人问过了。 也许这会有所作为。
不。但是,大多数时候他们会。
虽然将const
视为“线程安全”并将mutable
视为“(已经)线程安全”是有帮助的,但const
仍然从根本上与承诺“我不会改变这个值”的概念联系在一起。 永远都是。
我有一个很长的思路,所以请耐心等待。
在我自己的编程中,我把const
放在任何地方。 如果我有一个价值,除非我说我想改变它,否则改变它是一件坏事。 如果你试图有目的地修改一个 const 对象,你会得到一个编译时错误(容易修复并且没有可交付的结果!)。 如果不小心修改了非常量对象,则会出现运行时编程错误、已编译应用程序中的错误以及头痛。 所以最好在前一方面犯错并保持const
。
例如:
bool is_even(const unsigned x)
{
return (x % 2) == 0;
}
bool is_prime(const unsigned x)
{
return /* left as an exercise for the reader */;
}
template <typename Iterator>
void print_special_numbers(const Iterator first, const Iterator last)
{
for (auto iter = first; iter != last; ++iter)
{
const auto& x = *iter;
const bool isEven = is_even(x);
const bool isPrime = is_prime(x);
if (isEven && isPrime)
std::cout << "Special number! " << x << std::endl;
}
}
为什么is_even
和is_prime
的参数类型标记为const
? 因为从实现的角度来看,更改我正在测试的数字将是一个错误! 为什么const auto& x
? 因为我不打算更改该值,并且我希望编译器在我更改时对我大喊大叫。 同样的, isEven
和isPrime
:本次测试的结果应该不会改变,所以强制执行。
当然, const
成员函数只是给this
一个const T*
形式的类型的方法。 它说“如果我要改变我的一些成员,这将是一个实施错误”。
mutable
说“除了我”。 这就是“逻辑常量”的“旧”概念的来源。 考虑他给出的常见用例:互斥锁成员。 你需要锁定这个互斥锁以确保你的程序是正确的,所以你需要修改它。 但是,您不希望该函数是非常量的,因为修改任何其他成员都是错误的。 因此,您将其设为const
并将互斥锁标记为mutable
。
这些都与线程安全无关。
我认为说新定义取代了上面给出的旧想法有点过头了。 他们只是从另一个角度补充它,即线程安全。
现在 Herb 的观点是,如果你有const
函数,它们需要是线程安全的,才能被标准库安全使用。 因此,您应该真正标记为mutable
的唯一成员是那些已经是线程安全的成员,因为它们可以从const
函数进行修改:
struct foo
{
void act() const
{
mNotThreadSafe = "oh crap! const meant I would be thread-safe!";
}
mutable std::string mNotThreadSafe;
};
好的,所以我们知道线程安全的东西可以被标记为mutable
,你问:他们应该吗?
我认为我们必须同时考虑这两种观点。 从 Herb 的新观点来看,是的。 它们是线程安全的,因此不需要受限于函数的常量性。 但是仅仅因为它们可以安全地摆脱const
的约束并不意味着它们必须如此。 我还需要考虑:如果我确实修改了那个成员,它会不会在实现中出错? 如果是这样,它不需要是mutable
!
这里有一个粒度问题:一些函数可能需要修改潜在的mutable
成员,而其他函数则不需要。 这就像只希望某些函数具有类似友元的访问权限,但我们只能将整个类添加为友元。 (这是一个语言设计问题。)
在这种情况下,您应该站在mutable
一边犯错。
当 Herb 给出一个const_cast
示例并声明它是安全的时,他说得有点过于松散了。 考虑:
struct foo
{
void act() const
{
const_cast<unsigned&>(counter)++;
}
unsigned counter;
};
这在大多数情况下是安全的,除非foo
对象本身是const
:
foo x;
x.act(); // okay
const foo y;
y.act(); // UB!
这在 SO 的其他地方有介绍,但是const foo
意味着counter
成员也是const
,并且修改const
对象是未定义的行为。
这就是为什么你应该在mutable
方面犯错: const_cast
并没有给你同样的保证。 如果counter
被标记为mutable
,它就不会是一个const
对象。
好的,所以如果我们需要它在一个地方mutable
,那么我们在任何地方都需要它,我们只需要在不需要的情况下小心。 这当然意味着所有线程安全成员都应该被标记为mutable
吗?
不,因为并非所有线程安全成员都用于内部同步。 最简单的例子是某种包装类(并不总是最佳实践,但它们存在):
struct threadsafe_container_wrapper
{
void missing_function_I_really_want()
{
container.do_this();
container.do_that();
}
const_container_view other_missing_function_I_really_want() const
{
return container.const_view();
}
threadsafe_container container;
};
这里我们包装了threadsafe_container
并提供了另一个我们想要的成员函数(在实践中作为自由函数会更好)。 这里不需要mutable
,从旧的观点来看,正确性完全胜过:在一个函数中,我正在修改容器,这没关系,因为我没有说我不会(省略const
),而在另一个const
中我'我没有修改容器并确保我遵守承诺(省略mutable
)。
我认为 Herb 是在争论我们使用mutable
的大多数情况,我们也在使用某种内部(线程安全)同步对象,我同意。 因此,他的观点大部分时间都有效。 但是存在这样的情况,我只是碰巧有一个线程安全对象,而只是将它视为另一个成员; 在这种情况下,我们回到const
的旧和基本用法。
我刚刚看了演讲,我并不完全同意赫伯·萨特的说法。
如果我理解正确,他的论点如下:
[res.on.data.races]/3
对与标准库一起使用的类型提出了要求——非常量成员函数必须是线程安全的。
因此const
等效于线程安全。
如果const
等价于线程安全,那么mutable
必须等价于“相信我,即使这个变量的非常量成员也是线程安全的”。
在我看来,这个论点的所有三个部分都有缺陷(第二部分存在严重缺陷)。
1
的问题在于[res.on.data.races]
给出了标准库中类型的要求,而不是标准库中使用的类型。 也就是说,我认为将[res.on.data.races]
解释为也给出了与标准库一起使用的类型的要求是合理的(但并不完全明确),因为对于库来说这实际上是不可能的如果const
成员函数能够修改对象,则实现不通过const
引用修改对象的要求。
与关键问题2
是,虽然这是事实(如果我们接受1
)该const
必须意味着线程安全的,这是不正确的线程安全意味着const
,所以这两者是不等价的。 const
仍然意味着“逻辑上不可变”,只是“逻辑上不可变”的范围已经扩展到需要线程安全。
如果我们认为const
和线程安全是等价的,我们就会失去const
的优点,那就是它允许我们通过查看可以修改值的位置来轻松推理代码:
//`a` is `const` because `const` and thread-safe are equivalent.
//Does this function modify a?
void foo(std::atomic<int> const& a);
此外, [res.on.data.races]
的相关部分谈到了“修改”,可以在更一般的意义上合理地解释“以外部可观察的方式进行更改”,而不仅仅是“线程中的更改”-不安全的方式”。
3
的问题很简单,只有当2
为真时它才能为真,而2
存在严重缺陷。
因此,要将其应用于您的问题 - 不,您不应该使每个内部同步对象mutable
。
您应该为不影响对象外部可见状态的成员变量保留mutable
的。 另一方面(这是 Herb Sutter 在他的演讲中提出的关键点),如果您有一个由于某种原因可变的成员,则该成员必须在内部同步,否则您可能会使const
不意味着线程安全,这会导致标准库出现未定义的行为。
让我们谈谈const
的变化。
void somefunc(Foo&);
void somefunc(const Foo&);
在 C++03 及const
版本中,与非const
版本相比, const
版本为调用者提供了额外的保证。 它承诺不修改其参数,在这里通过修改我们的意思是叫Foo
的非const成员函数(包括分配等),或者将它传递给那些期望非功能const
参数,或做同样其裸露的非易变数据成员。 somefunc
将自身限制为Foo
上的const
操作。 而额外的保证完全是片面的。 调用者和Foo
提供者都不需要做任何特殊的事情来调用const
版本。 任何能够调用非const
版本的人也可以调用const
版本。
在 C++11 中,这发生了变化。 const
版本仍然为调用者提供相同的保证,但现在它带来了代价。 Foo
的提供者必须确保所有const
操作都是线程安全的。 或者至少当somefunc
是标准库函数时它必须这样做。 为什么? 因为标准库可以并行化它的操作,它会在没有任何额外同步的情况下对任何东西调用const
操作。 因此,您,用户,必须确保不需要这种额外的同步。 当然,在大多数情况下这不是问题,因为大多数类没有可变成员,并且大多数const
操作不涉及全局数据。
那么现在mutable
是什么意思呢? 和以前一样! 也就是说,这个数据是非常量的,但它是一个实现细节,我保证它不会影响可观察的行为。 这意味着不,您不必像在 C++98 中那样标记所有可见的东西mutable
一样。 那么什么时候应该将数据成员标记为mutable
呢? 就像在 C++98 中一样,当你需要从一个const
方法调用它的非const
操作时,你可以保证它不会破坏任何东西。 重申:
mutable
。 第一个条件是强加的,就像在 C++98 中一样,因为其他代码,包括标准库,可能会调用您的const
方法,并且没有人应该观察到此类调用导致的任何更改。 第二个条件存在,这就是 C++11 中的新功能,因为此类调用可以异步进行。
接受的答案涵盖了这个问题,但值得一提的是,萨特后来更改了错误地建议 const == mutable == thread-safe 的幻灯片。 可以在此处找到导致该幻灯片更改的博客文章:
TL:DR Const 和 Mutable 都意味着线程安全,但在程序中可以更改和不可以更改的内容方面具有不同的含义。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.