在多线程环境中使用 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->join()
之后运行吗? IOW,还有一个正在运行的线程吗?
@MSalters:不一定。线程可能在连接完成之前已经完成。如果线程尚未退出,则连接只会等待线程退出。
好吧,现在我明白了。这解释了你的情况。我同意,这可能是正确的解释。
@VaughnCato 你是对的,当我将行 auto output = make_shared以上是关于在多线程环境中使用 std::string 时 Clang 的线程清理器警告的主要内容,如果未能解决你的问题,请参考以下文章
std::move 将 std::string 移动到另一个线程时出错