gcc 4.3版本下的std::string是否支持线程安全?

25
我正在开发一个在Linux上运行的多线程程序(使用G++ 4.3编译),如果你搜索一下,你会发现关于std::string在GCC中不是线程安全的许多可怕故事。这似乎是由于它内部使用了写时复制而导致像Helgrind这样的工具出现问题。
我已经编写了一个小程序,将一个字符串复制到另一个字符串中,如果检查两个字符串,它们都共享相同的内部_M_p指针。当一个字符串被修改时,指针会改变,因此写时复制功能正常工作。
但我担心的是,如果我在两个线程之间共享一个字符串(例如将其作为对象传递给线程安全的数据队列),会发生什么。我已经尝试使用“-pthread”选项编译,但似乎没有太大的区别。所以我的问题是:
- 有没有办法强制std::string成为线程安全的?如果禁用写时复制行为可以实现这一点,我也不介意。 - 其他人是如何解决这个问题的?还是我太过紧张?
我似乎找不到一个明确的答案,所以希望你们可以帮助我。
  _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 个好问题!不幸的是,人们只看到“线程安全”就想到“不行!”他们最好读完整个问题! :) - sellibitze
由于std :: string是模板std :: basic_string的一个实例,因此您可以查看源代码。 尝试查找任何可以打开/关闭线程安全性的宏。 - Leandro T. C. Melo
顺便提一下,在多线程环境中,写时复制是很慢的,你应该不想使用它,而不是愿意使用。 - GManNickG
参见https://dev59.com/bXI-5IYBdhLWcg3w0MDx#1661251 - Zan Lynx
6个回答

15

线程目前尚未成为C++标准的一部分。但我认为,在当今这个时代,任何供应商都不可能没有让std::string线程安全的措施。注意:“线程安全”的定义有所不同,我的定义可能与你的不同。当然,如果你不需要保护像std::vector这样的容器以进行并发访问,那么默认情况下保护它是没有意义的。这将违反C++中“不用就不付费”的精神。如果用户想在不同的线程之间共享对象,他/她始终应该负责同步。问题在于,一个库组件是否使用和共享一些隐藏的数据结构,即使从用户的角度看,“函数应用于不同的对象”,也可能导致数据竞争。

C++0x草案(N2960)包含了“避免数据竞争”的章节,基本上说,只有在库组件积极避免可能的数据竞争时,才能访问用户无法看到的共享数据。这听起来像是std::basic_string的写时复制实现必须与另一个内部数据从未在不同字符串实例之间共享的实现一样安全,涉及多线程。

我不确定libstdc++是否已经处理过这个问题。我认为它已经处理过了。为了确认,可以查看文档


1
非常感谢您提供的详细答案。我查看了您提供的页面,但我认为它仍然有点模糊,只是一般性地讨论容器(即您应该提供足够的锁定),并没有涉及字符串方面的内容。避免数据竞争似乎至少是一个“借口”,可以假设在线程之间一切都会顺利进行,因为这完全将责任放在实现库的人身上(当然,前提是作为程序员,我通过值而不是引用传递字符串)。 - Benjamin

11

如果您不介意禁用写时复制,这可能是最好的做法。std::string的写时复制仅在知道自己正在复制另一个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*,无法确定它来自何处或其生命周期是什么,所以它必须创建一个新的私有副本)。


12
小细节提示:使用c_str()的结果进行赋值,如果原字符串包含空字符,则会截断字符串。更安全的做法是使用接受const char*和size_type参数的assign方法(或构造函数),并将"orig.data()"和"orig.size()"传递给它。 - Éric Malenfant
如果我决定选择“禁用COW”路线,我一定会使用这个(以及Eric的附加备注)。做得好 :) - Benjamin
@Eric 很好的观点,我简直不敢相信我没有考虑它如何与空值交互。谢谢。 - Jack Lloyd
或者,你可以使用迭代器初始化的方式。 - Matthieu M.

3

这个 libstdc++ 内部的部分说明:

C++ 库的字符串功能需要一些原子操作来提供线程安全性。如果您不采取任何特殊措施,库将使用这些函数的存根版本,这些版本不是线程安全的。它们可以正常工作,除非您的应用程序是多线程的。

引用计数应该在多线程环境下工作。(除非您的系统没有提供必要的原子操作)


如果有一种快速方式可以告诉我们是使用了真实函数还是存根函数,那就太好了。也许可以查看机器码并检查是否有 LOCK 前缀... - Zan Lynx

0
根据此错误问题std::basic_string的写时复制实现仍然不完全线程安全。<ext/vstring.h>是一种没有COW的实现,似乎在只读环境下表现更好。

0

没有STL容器是线程安全的。这样,库就具有了通用性(既可以在单线程模式下使用,也可以在多线程模式下使用)。在多线程中,您需要添加同步机制。


3
与字符串不同的是,即使通过值传递它也会引起线程安全问题-这可能相当令人意外,至少可以这么说。 - Jack Lloyd
1
感谢大家的快速回答!我知道没有任何STL容器是线程安全的(并且在需要线程安全时会正确使用锁定包装器),但就我所知,std::string是唯一一个“秘密”使用相同数据存储区的容器。我的担心是,复制-写入的记账方式根本不是线程安全的,这样说对吗? - Benjamin
9
我不明白为什么这个会得到那么多赞。各位,请注意实际问题。 - sellibitze

0

我认为那个错误是指iostreams代码,而不是字符串代码? - Benjamin
哦,对了。我提到的这个问题与basic_string (#5444)有关,但它已经被关闭了,因为和5432有相同的解决方案。我编辑了我的回答以澄清这一点。 - Éric Malenfant
谢谢Eric!只要我们添加缺陷跟踪器项目,这个似乎也相关:http://gcc.gnu.org/bugzilla/show_bug.cgi?id=40518 - Benjamin
我在这个主题上找到了另一个大讨论:http://etbe.coker.com.au/2009/06/22/valgrindhelgrind-and-stl-string/ - Benjamin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接