使用 gcc 4.3 时 std::string 是线程安全的吗?
Posted
技术标签:
【中文标题】使用 gcc 4.3 时 std::string 是线程安全的吗?【英文标题】:Is std::string thead-safe with gcc 4.3? 【发布时间】:2009-10-20 13:57:13 【问题描述】:我正在开发一个在 Linux 上运行的多线程程序(使用 G++ 4.3 编译),如果您搜索一下,您会发现很多关于 std::string 不是 GCC 线程安全的可怕故事。这可能是因为它在内部使用了写时复制,这对 Helgrind 等工具造成了严重破坏。
我制作了一个小程序,将一个字符串复制到另一个字符串,如果您检查两个字符串,它们都共享相同的内部 _M_p 指针。当一个字符串被修改时,指针会改变,所以写时复制的东西工作正常。
但我担心的是,如果我在两个线程之间共享一个字符串会发生什么(例如,将它作为对象传递给两个线程之间的线程安全数据队列)。我已经尝试使用“-pthread”选项进行编译,但这似乎没有太大区别。所以我的问题:
有没有办法强制 std::string 线程安全?我不介意是否禁用写时复制行为来实现这一点。 其他人是如何解决这个问题的?还是我偏执?我似乎找不到明确的答案,所以我希望你们能帮助我..
编辑:
哇,这么短的时间就有这么多答案。谢谢!当我想禁用 COW 时,我肯定会使用 Jack 的解决方案。但现在主要问题变成了:我真的必须禁用 COW 吗?还是为 COW 线程安全地进行了“簿记”?我目前正在浏览 libstdc++ 源代码,但这需要相当长的时间才能弄清楚...
编辑 2
OK 浏览了 libstdc++ 源代码,我在 libstd++-v3/include/bits/basic_string.h 中找到了类似的内容:
_CharT*
_M_refcopy() throw()
#ifndef _GLIBCXX_FULLY_DYNAMIC_STRING
if (__builtin_expect(this != &_S_empty_rep(), false))
#endif
__gnu_cxx::__atomic_add_dispatch(&this->_M_refcount, 1);
return _M_refdata();
// XXX MT
所以肯定有一些关于引用计数器的原子更改...
结论
我在这里将sellibitze 的评论标记为答案,因为我认为我们已经得出结论,该领域目前仍未解决。为了规避 COW 行为,我建议 Jack Lloyd 的回答。谢谢大家的有趣讨论!
【问题讨论】:
+1 好问题!不幸的是,人们只是阅读“线程安全”并认为“不!”。他们最好阅读整个问题! :) 由于 std::string 是模板 std::basic_string 的一个实例,您可以查看源代码。尝试寻找任何可以打开/关闭线程安全的宏。 顺便说一下,copy-on-write 在多线程环境下很慢,你应该想不使用它,不愿意。 另见***.com/questions/1661154/… 【参考方案1】:线程还不是标准的一部分。但我不认为现在任何供应商都可以在不使 std::string 线程安全的情况下逃脱。注意:“线程安全”有不同的定义,我的可能与您的不同。当然,默认情况下保护像 std::vector 这样的容器以进行并发访问是没有意义的,即使您不需要它也是如此。这将违背 C++ 的“不为不使用的东西付费”的精神。如果用户想要在不同线程之间共享对象,他/她应该始终负责同步。这里的问题是库组件是否使用和共享一些可能导致数据竞争的隐藏数据结构,即使从用户的角度来看“函数应用于不同的对象”。
C++0x 草案 (N2960) 包含“避免数据竞争”部分,该部分基本上说库组件可以访问对用户隐藏的共享数据,当且仅当它主动避免可能的数据竞争时。听起来 std::basic_string 的写时复制实现必须与 w.r.t 一样安全。多线程作为另一种实现,其中内部数据永远不会在不同的字符串实例之间共享。
我不能 100% 确定 libstdc++ 是否已经处理了它。我认为确实如此。可以肯定的是,请查看the documentation
【讨论】:
非常感谢您的详细回答。我检查了你链接的页面,但在我看来它仍然有点模糊,一般来说是谈论容器(你应该提供足够的锁定),而不是关于字符串。 :) 避免数据竞争似乎至少是一个“借口”,可以继续假设线程之间的一切都会好起来,因为这完全将责任放在了实现库的人身上(当然,前提是我作为程序员传递字符串按值而不是按引用)...【参考方案2】:如果您不介意禁用写时复制,这可能是最好的做法。 std::string 的 COW 仅在它知道它正在复制另一个 std::string 时才有效,因此您可以使其始终分配新的内存块并进行实际复制。例如这段代码:
#include <string>
#include <cstdio>
int main()
std::string orig = "I'm the original!";
std::string copy_cow = orig;
std::string copy_mem = orig.c_str();
std::printf("%p %p %p\n", orig.data(),
copy_cow.data(),
copy_mem.data());
将显示第二个副本(使用 c_str)阻止 COW。 (因为 std::string 只看到一个裸露的 const char*,并且不知道它来自哪里或它的生命周期可能是什么,所以它必须制作一个新的私有副本)。
【讨论】:
小警告:如果 orig 包含嵌入的空值,分配给 c_str() 的结果将截断字符串。使用采用 const char* 和 size_type 的 assign 方法(或构造函数)并将“orig.data()”和“orig.size()”传递给它会更安全。 如果我决定采用“禁用 COW”路线,我肯定会使用这个(以及 Eric 的补充说明)。做得很好:) @Eric 太好了,我不敢相信我没有考虑过它会如何与空值交互。谢谢。 或者,我认为您可以使用迭代器的初始化。【参考方案3】:This 部分 libstdc++ 内部状态:
C++ 库字符串功能 需要几个原子操作 提供线程安全。如果你不 采取任何特殊行动,图书馆 将使用这些的存根版本 不是线程安全的函数。 它们会正常工作,除非您的 应用程序是多线程的。
引用计数应该在多线程环境中工作。 (除非你的系统没有提供必要的原子)
【讨论】:
如果有一种快速的方法来判断是否使用了真实函数或存根函数,那就太好了。也许查看机器代码并检查 LOCK 前缀...【参考方案4】:没有 STL 容器是线程安全的。这样,该库具有通用用途(既可用于单线程模式,也可用于多线程模式)。在多线程中,您需要添加同步机制。
【讨论】:
与字符串的不同之处在于,即使按值传递它也会引发线程问题——至少可以说,这可能有点出乎意料。 感谢大家这么快的回答!我知道没有 STL 容器是线程安全的(并且当我需要它们是线程安全的时正确使用锁定包装器),但据我所知,std::string 是唯一一个“秘密”为相同字符串使用相同数据存储的容器。我担心的是写时复制簿记根本不是线程安全的,我说得对吗? 我不明白为什么这会得到如此多的支持。小伙伴们,请注意实际问题【参考方案5】:这似乎是不久前修复的:http://gcc.gnu.org/bugzilla/show_bug.cgi?id=5444 是(与 http://gcc.gnu.org/bugzilla/show_bug.cgi?id=5432 相同的问题关闭,后者在 3.1 中已修复)。
另见http://gcc.gnu.org/bugzilla/show_bug.cgi?id=6227
【讨论】:
我认为这个 bug 指的是 iostreams 代码,而不是字符串代码? 哦,对了。我将此称为与 basic_string (#5444) 相关的一个,因为与 5432 具有相同的分辨率而被关闭。我编辑了我的答案以澄清这一点。 谢谢埃里克!只要我们添加 bugtracker 项,这一项似乎也相关:gcc.gnu.org/bugzilla/show_bug.cgi?id=40518 我发现了另一个关于这个主题的大讨论:etbe.coker.com.au/2009/06/22/valgrindhelgrind-and-stl-string【参考方案6】:根据this bug issue,std::basic_string
的写时复制实现仍然不是完全线程安全的。 <ext/vstring.h>
是一个没有 COW 的实现,并且似乎在只读上下文中做得更好。
【讨论】:
以上是关于使用 gcc 4.3 时 std::string 是线程安全的吗?的主要内容,如果未能解决你的问题,请参考以下文章