【问题标题】:Is using `std::get<I>` on a `std::tuple` guaranteed to be thread-safe for different values of `I`?在 `std::tuple` 上使用 `std::get<I>` 是否保证对于不同的 `I` 值是线程安全的?
【发布时间】:2017-04-12 06:11:13
【问题描述】:

假设我有

std::tuple<T0, T1, T2> my_tuple{x0, x1, x2};

其中T0T1T2 是值类型(即不可能有别名)

访问my_tuple的元素并使用std::get从多个线程同时改变它们是否安全,只要每个线程访问不同的元素?

例子:

template <typename T>
void process(T& x) { /* mutate `x` */ }

// ...

std::thread{[&]{ process(std::get<0>(my_tuple)); }}.detach();
std::thread{[&]{ process(std::get<1>(my_tuple)); }}.detach();
std::thread{[&]{ process(std::get<2>(my_tuple)); }}.detach();

本能地我会说它是安全的,因为my_tuple 可以被认为是struct { T0 x0; T1 x1; T2 x2; };...但它是由标准保证的吗?

【问题讨论】:

  • std::getconstexpr,所以可以认为它等同于直接访问,只要线程访问不同的元素,这就是线程安全的。
  • @SamVarshavchik 有一个通用规则,即在没有同步的情况下访问两个不同线程中的数据是未定义的行为;是什么阻止 std::get 算作对整个 std::tuple 对象的访问?我不是说它是,我只是不相信标准提供了保证。
  • @Yakk 从多个线程读取是安全的(afaik),如果线程写入超过零,则为 UB。 get on my_tuple 不会修改 my_tuple 并且可以提出最终修改将对其进行分离子对象的论点。
  • @krzaq std::get 是“正在阅读”,因为它将其参数作为非const 引用?我的意思是,它应该正在阅读,但标准是否同意?我们可能不得不依靠“标准库不会做不必要的事情”。但是举个例子,假设我们有一个包含两个空类的元组,并且元组被压缩了。现在我们有两个相同的位置对象以非const 的方式使用。标准可以吗?我不确定。我认为它应该是,我不确定它是否
  • @W.F.我怀疑这里的标准是健全的,但我不知道。我只是想指出这里有一个真正的问题。对于容器,标准在需要同步方面是明确的(const 是相互线程安全的),甚至列出了就同步而言被“视为 const”的显式非 const 方法。元组没有;这可能意味着该标准的其他部分足以保证正常行为,或者它可能表明存在标准缺陷。 无论如何,在实践中使用是安全的;没有编译器愚蠢到可以打破这一点。

标签: c++ multithreading c++11 thread-safety stdtuple


【解决方案1】:

由于std::get 在规范中没有关于其数据竞争属性的明确声明,我们回退到 [res.on.data.races] 中定义的默认行为。具体来说,第 2 和第 3 段讲述了这个故事:

C++ 标准库函数不得直接或间接访问可由当前线程以外的线程访问的对象 (1.10),除非这些对象是通过函数的参数直接或间接访问的, 包括this

AC ++ 标准库函数不得直接或间接修改当前线程以外的线程可访问的对象 (1.10),除非通过函数的非const 参数直接或间接访问对象,包括this

这些仅针对与函数参数提供的对象不同的用途提供数据竞争保护。模板参数在技术上不是函数的参数,所以它不合格。

您的案例涉及多个线程将同一对象传递给不同的get 调用。由于您传递的是非const 参数,因此将假定get 正在修改其tuple 参数。因此,在同一个对象上调用get 算作从多个线程修改对象。因此,调用它可以合法地在 tuple 上引发数据竞争。

尽管从技术上讲,它只是从tuple 中提取一个子对象,因此不应干扰对象本身或其其他子对象。标准不知道这一点。

然而,如果参数是const,则get 不会被视为与其他constget 的调用引发数据竞争。这些只是从多个线程查看同一个对象,这在标准库中是允许的。这将引发与get 的非const 使用或tuple 对象的其他非const 使用的数据竞争。但不适用于 const 使用它。

因此您可以“访问”它们,但不能“修改”它们。

【讨论】:

  • @gigabytes:为了获得指向tuple 的子对象的指针/引用,您必须在该元组对象上调用标准库函数。因此,获取该子对象的数据竞争行为是由标准库的函数调用行为定义的,如上所述。如果你调用非constget,那么这个调用被认为与整个元组对象的任何其他修改没有区别。没有标准的措辞让它在数据竞争行为方面将此调用视为与将非const 元组传递给任何函数的任何不同。
  • 换句话说,obj.aobj.b 永远不会引发数据竞赛。但这是因为语言明确说明了这一点。 func&lt;0&gt;(obj)func&lt;1&gt;(obj) 会引发数据竞争,即使 func&lt;0&gt; 恰好返回 obj.afunc&lt;1&gt; 恰好返回 obj.b。现在显然,在这种情况下,实际上没有任何实现会失败。但我们谈论的是标准所说的,而不是实现如何实现它。
  • 仍然没有:)“获取该子对象的数据竞争行为由标准库的函数调用行为定义,如上所述”这是错误的。同样,引用的部分只说 you 保证标准库函数基本上不会访问除其参数以外的任何内容。它没有将任何事情定义为数据竞赛。数据竞赛的定义在标准的另一部分,你必须看看它(再说一次,我现在不能看,我会发布一个完整的答案,我保证)。
  • @gigabytes 像std::vector 这样的容器有special wording w/r/t data races
  • @gigabytes 元组不是容器
【解决方案2】:

简短的回答是它取决于类型以及process 的作用而不是getget 本身仅检索对象的地址并将其作为引用返回。检索地址主要只是读取整数的内容。它不会引发竞争条件。粗略地说,你问题中的代码 sn-p 是线程安全的当且仅当以下是线程安全的,

T1 t1;
T2 t2;
T3 t3;

std::thread{[&]{process(t1);}}.detach();
std::thread{[&]{process(t2);}}.detach();
std::thread{[&]{process(t3);}}.detach();

【讨论】:

  • 你能证明吗?重点是我认为标准中没有任何保证可以保证在不同线程中改变std::tuple::get返回的不同引用是线程安全的。它很可能适用于std::tuple 的所有主要实现,但它是否受到标准的正式保证?
  • @VittorioRomeo 关键是,它与gettuple 无关。对象的地址一旦创建就不会改变。一个人可能会错误地写入该地址,但不能更改该地址本身。因此,get 本身是安全的。
  • get 在同一个元组上不是线程安全的。标准中没有任何内容要求实现以线程安全的方式在不同对象的同一元组上实现 get
猜你喜欢
  • 2010-10-15
  • 1970-01-01
  • 2019-06-15
  • 2011-05-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多