在多线程环境中使用 std::string 时 Clang 的线程清理器警告

Posted

技术标签:

【中文标题】在多线程环境中使用 std::string 时 Clang 的线程清理器警告【英文标题】:Clang's thread sanitizer warning while using std::string in a multi-threaded environment 【发布时间】:2015-01-01 10:58:33 【问题描述】:

在使用 clang 的线程清理器时,我们注意到数据竞争警告。我们认为这是由于 std::string 的写时复制技术不是线程安全的,但我们可能错了。我们将看到的警告减少到此代码:

void test3() 
  std::unique_ptr<std::thread> thread;

  
    auto output = make_shared<string>();
    std::string str = "test";
    thread.reset(new std::thread([str, output]()  *output += str; ));
    // The str string now goes out of scope but due to COW
    // the captured string may not have the copy of the content yet.
  

  thread->join();

在启用线程清理器的情况下编译时:

clang++ -stdlib=libc++ -std=c++11 -O0 -g -fsanitize=thread -lpthread -o test main.cpp

clang++ -std=c++11 -O0 -g -fsanitize=thread -lpthread -o test main.cpp

当多次运行时,它最终会产生这个警告:

WARNING: ThreadSanitizer: data race (pid=30829)
  Write of size 8 at 0x7d0c0000bef8 by thread T62:
    #0 operator delete(void*) <null>:0
    ...

  Previous write of size 1 at 0x7d0c0000befd by thread T5:
    #0 std::__1::char_traits<char>::assign(char&, char const&) string:639
    ...

这是线程清理程序的误报还是真正的数据竞争?如果是后者, 是否可以在不更改代码的情况下解决它(例如,通过将一些标志传递给编译器),这是字符串实现(或其他)中的已知错误吗?

更新:clang --version 输出:

Ubuntu clang version 3.5-1ubuntu1 (trunk) (based on LLVM 3.5)
Target: x86_64-pc-linux-gnu
Thread model: posix

更新:The cpp 我用来重现此警告。

【问题讨论】:

libc++ 不应该使用 COW 字符串,所以如果 COW 字符串是造成这种情况的原因,我会感到惊讶。 @BartoszKP,是的,抱歉,这是一个 COW 问题,只是我们的猜测得到了测试用例推理的支持。本质上,我们需要的是使代码线程安全,问题是有时我们会从内部收到类似的警告,例如boost::asio 所以我们并不总是能够更改代码。 clang 的线程清理器是一个很棒的工具,但是很难正确解释各种互斥锁和其他同步技术。可能就是这种情况 - 我从中得到了一些误报,例如,它不理解 Qt 的互斥锁。 【参考方案1】:

[编辑] 下面的假设被证明是错误的,请参阅 cmets 中的链接。 T5,而不是 T62 是上面代码中产生的线程。

了解线程 ID 会很有用,但我假设 T5 是主线程,T62 是衍生线程。看起来副本是在主线程上制作的(在新线程生成之前)并在新线程上销毁(显然)。这是安全的,因为新线程在存在之前不能与主线程竞争。

因此,这是一个线程清理程序错误。未能检查上一次写入时是否存在线程 T62。

【讨论】:

是的,线程 T5 似乎是启动 test3 函数的线程,而 T62 似乎是执行 lambda 的线程。 Here is the full TSan warning。所以看来你是对的,如果你不介意,我会再给这个问题多一点时间,然后再标记答案。 该警告相当复杂。有了完整的上下文,很明显 T5 和 T62 都不是主线程(!)相反,您似乎有一个 test2() 碰巧使用了相同的内存。 Here 是用于生成警告的 cpp 代码(行号不匹配,抱歉)。没错,T5 和 T62 都不是主线程,我在单独的线程中多次运行 test3 函数以更快地重现警告。不过,每个 test3 调用之间没有共享内存,所以我不认为有太大区别(?)。 (最初我在那个 cpp 文件中也有 test1 和 test2 函数,但它们正在测试其他东西,它们之间根本没有任何共享内存)。【参考方案2】:

这很棘手。我在下面的代码中总结了逻辑:

在线程 T62 中: 创建字符串 s(带引用计数) 在 T62 的线程存储中创建指向 s 的 output_1 创建线程 T5 在 T5 的线程存储中创建指向 s 的 output_2 同步点 在线程 T5 中: 附加到 ** 修改 ** s 的引用计数的线程安全递减(不是同步点) output_2 生命周期结束 出口 在线程 T62 中: s 的引用计数的线程安全递减(不是同步点) output_1 生命周期结束 释放 ** 修改 ** 加入 同步点 在线程 T62 中: 摧毁 T5

据我所知,该标准不保证调用shared_ptr 删除器的同步:

(20.8.2.2/4) 为确定是否存在数据竞争,成员函数应仅访问和修改 shared_ptr 和 weak_ptr 对象本身,而不是它们引用的对象。

我认为这意味着在调用 shared_ptr 的成员函数时实际发生在指向对象上的任何修改,例如删除器可能进行的任何修改,都被认为超出了 @987654323 的范围@,因此shared_ptr 没有责任确保他们不会引入数据竞争。例如,当线程 T62 试图销毁它时,T5 对字符串所做的修改可能对 T62 不可见。

然而,Herb Sutter 在他的“原子武器”演讲中表示,他认为在没有获取和释放语义的情况下,在 shared_ptr 析构函数中原子减少引用计数是一个错误,但我我不确定它是如何违反标准的。

【讨论】:

我有点不知所措。那个 dtor 不是在thread-&gt;join() 之后运行吗? IOW,还有一个正在运行的线程吗? @MSalters:不一定。线程可能在连接完成之前已经完成。如果线程尚未退出,则连接只会等待线程退出。 好吧,现在我明白了。这解释了你的情况。我同意,这可能是正确的解释。 @VaughnCato 你是对的,当我将行 auto output = make_shared(); 移动到本地范围的左括号上方(一行)到推迟销毁输出字符串,使用 libc++ 时警告消失。不幸的是,当我使用 libstdc++ 时,我仍然看到警告,it looks a tiny bit different(直到现在才发现)。 @PeterJankuliak:我不确定您链接到的页面上的警告是在移动线路之前还是之后,还是改变了?

以上是关于在多线程环境中使用 std::string 时 Clang 的线程清理器警告的主要内容,如果未能解决你的问题,请参考以下文章

std::move 将 std::string 移动到另一个线程时出错

在多线程环境中使用 PyCurl 时程序消耗的内存不断增长

如何保护可能在多线程或异步环境中使用的资源?

在线程中使用 std::string 函数是不是安全? (c++)

在 std::map clear() 中崩溃 - 多线程

在多线程环境中写入时的 Swift 数组复制