【问题标题】:std::string in a multi-threaded program多线程程序中的 std::string
【发布时间】:2009-11-02 12:55:14
【问题描述】:

鉴于:

1) C++03 标准没有以任何方式解决线程的存在

2) C++03 标准让实现来决定 std::string 是否应该在其复制构造函数中使用 Copy-on-Write 语义

3) Copy-on-Write 语义通常会导致多线程程序中出现不可预知的行为

我得出以下看似有争议的结论:

您根本无法在多线程程序中安全且可移植地使用 std::string

显然,没有 STL 数据结构是线程安全的。但至少,以 std::vector 为例,您可以简单地使用互斥锁来保护对向量的访问。使用使用 COW 的 std::string 实现,如果不在供应商实现的深处编辑引用计数语义,您甚至无法可靠地做到这一点。

现实世界的例子:

在我的公司,我们有一个多线程应用程序,它已经过彻底的单元测试并无数次地通过 Valgrind 运行。该应用程序运行了几个月,没有任何问题。有一天,我在另一个版本的 gcc 上重新编译了应用程序,突然间我总是得到随机的段错误。 Valgrind 现在在 std::string 复制构造函数中报告 libstdc++ 深处的无效内存访问。

那么解决方法是什么?好吧,当然,我可以 typedef std::vector<char> 作为字符串类 - 但实际上,这很糟糕。我也可以等待 C++0x,我祈祷这将要求实现者放弃 COW。或者,(颤抖),我可以使用自定义字符串类。我个人总是反对那些实现自己的类的开发人员,但老实说,我需要一个字符串类,我可以肯定它没有使用 COW 语义;而 std::string 根本不保证这一点。

std::string 根本无法在可移植的多线程程序中完全可靠地使用,这对吗?什么是好的解决方法?

【问题讨论】:

  • 哇,我什至不知道 COW 字符串实现还在外面。
  • 如果您的 STL 实现使用非线程安全的 COW,您应该将其替换为更好的。这对我来说似乎是一个错误。
  • @sbi,不仅在外面,而且很常见。 GCC 4.3.2(从 2008 年 8 月开始)使用 COW。
  • std::vector<char> 有什么不好?

标签: c++ stl


【解决方案1】:

您无法在多线程程序中安全且可移植地执行任何操作。不存在可移植的多线程 C++ 程序,正是因为线程将 C++ 所说的所有关于操作顺序以及修改任何变量的结果都抛到了窗外。

标准中也没有任何内容可以保证vector 可以按照您所说的方式使用。提供具有线程扩展的 C++ 实现是合法的,其中,例如,在初始化它的线程之外使用向量会导致未定义的行为。当您启动第二个线程时,您不再使用标准 C++,您必须向您的编译器供应商咨询什么是安全的,什么不是。

如果您的供应商提供了线程扩展,并且还提供了一个带有 COW 的 std::string(因此)不能使其成为线程安全的,那么我认为目前您的论点是与您的供应商或线程有关扩展名,而不是 C++ 标准。例如,可以说 POSIX 应该在使用 pthread 的程序中禁止 COW 字符串。

您可以通过使用单个互斥锁来确保安全,您可以在执行任何字符串突变时使用该互斥锁,以及对作为复制结果的字符串的任何读取。但是您可能会在该互斥体上遇到严重的争论。

【讨论】:

    【解决方案2】:

    你是对的。这将在 C++0x 中修复。现在你必须依赖你的实现文档。例如,最近的 libstdc++ 版本 (GCC) 允许您使用字符串对象 就像 没有字符串对象与另一个对象共享其缓冲区。 C++0x 强制库实现以保护用户免受“隐藏共享”。

    【讨论】:

      【解决方案3】:

      鉴于标准没有提及内存模型并且完全不知道线程,我想说你不能肯定地假设每个实现都是非牛的,所以不,你不能

      除此之外,如果您了解您的工具,大多数实现将使用非牛字符串来允许多线程。

      【讨论】:

      • "大多数实现将使用非牛字符串来允许多线程。"不完全正确,事实上大多数 C++ 编译器实现了COW 字符串:gcc、intel、HP...MSVC 没有。大多数实现(MSVC6 除外)都是线程安全的,因为使用原子计数器。
      【解决方案4】:

      更正确的看待它的方式是“您不能在多线程环境中安全且可移植地使用 C++”。也不能保证其他数据结构也能正常运行。或者运行时不会炸毁您的计算机。该标准不保证任何关于线程。

      因此,要在 C++ 中使用线程执行任何操作,您必须依赖实现定义的保证。然后您可以安全地使用std::string,因为每个实现都会告诉您在线程环境中使用它是否安全。

      在您产生第二个线程的那一刻,您就失去了真正可移植性的所有希望。 std::string 并不比其他语言/库“更不便携”。

      【讨论】:

      • 你是对的,当然。如果我们谈论的是标准所保证的内容,那么没有多线程 C++ 程序是可移植的。然而,事情的事实上状态是,您可以编写多线程代码,使用诸如 pthread 或 boost 线程之类的东西在大多数现代平台上表现一致 - 使用 @987654323 的代码除外@,由于 COW 语义的不可忽略的可能性。或者换一种说法:使用std::vector 的多线程代码现在不太可能在任何地方中断;但std::string 则不能这样说。
      【解决方案5】:

      您可以使用 STLport。它提供非 COW 字符串。它在不同平台上具有相同的行为。

      article 提供了 STL 字符串与写时复制和非复制的比较 基于 STLport 字符串、绳索和 GNU libstdc++ 的写入时算法 实现。

      在我工作的一家公司中,我有一些在 HP-UX 11.31 上运行使用 STLport 和不使用 STLport 构建的相同服务器应用程序的经验。该应用程序是使用优化级别 O2 的 gcc 4.3.1 编译的。因此,当我运行使用 STLport 构建的程序时,它处理请求的速度比没有使用 STLport 构建的相同程序(使用 gcc 自己的 STL 库)快 25%。

      我分析了这两个版本,发现没有 STLport 的版本在pthread_mutex_unlock() 中花费的时间(2.5%)比使用 STLport 的版本(1%)要多得多。并且 pthread_mutex_unlock() 本身在没有 STLport 的版本中是从 std::string 函数之一调用的。

      但是,在分析之后,我以这种方式在最常调用的函数中更改了对字符串的赋值:

      string_var = string_var.c_str(); // added .c_str()
      

      没有 STLport 的版本的性能有显着提升。

      【讨论】:

        【解决方案6】:

        在 MSVC 中,std::string 不再是指向容器的引用计数共享指针。他们在每个复制构造函数和赋值运算符中选择按值传递整个内容,以避免多线程问题。

        【讨论】:

          【解决方案7】:

          我规范了字符串访问:

          • std::string 成员设为私有
          • 为吸气剂返回const std::string&
          • setter 修改成员

          这对我来说一直都很好,并且是正确的数据隐藏。

          【讨论】:

          • 返回一个常量引用能保证正确的行为吗?之前我也是这么想的,但是最近我被一些讨厌的并发问题所困扰,例如: const std::string& MyClass::GetString() const { MutexLock lock(m_Mutex);返回 m_MyString; } 在这种情况下,互斥体不是在返回值的复制构造函数之前解锁吗?在这种情况下,您可能会遇到竞争条件。
          • @the_mandrill:“在返回值的复制构造函数之前,互斥锁不会被解锁”当然在此之前它会被解锁。您所保护的只是创建参考。在这种情况下通过副本返回。
          • 这并不能解决问题。它与封装无关;问题要深得多。事实是,多线程程序不能永远修改已复制的std::string,并期望与所有符合标准的编译器一起正常工作。
          【解决方案8】:

          如果你想禁用 COW 语义,你可以强制你的字符串进行复制:

          // instead of:
          string newString = oldString;
          
          // do this:
          string newString = oldString.c_str();
          

          正如所指出的,特别是如果您可以嵌入空值,那么您应该使用迭代器 ctor:

          string newString(oldString.begin(), oldString.end());
          

          【讨论】:

          • 使用另一个实现似乎更容易更好(尤其是代码质量)。
          • 上面的代码调用strlen()来计算已知长度。更好string newString(oldString.begin(), oldString.end())
          • @Bill:如果字符串包含任何嵌入的空字符,这也会中断(这对于std::basic_string<> 衍生物是完全合法的)
          • @sehe:我很懒,只是离开了 Maxim 在 cmets 中的改进。我现在已将其添加到答案中,谢谢!
          猜你喜欢
          • 1970-01-01
          • 2023-01-11
          • 1970-01-01
          • 2012-03-07
          • 2022-01-14
          • 1970-01-01
          • 1970-01-01
          • 2021-10-02
          • 1970-01-01
          相关资源
          最近更新 更多