Helgrind:螺纹错误检测器

来源:互联网 发布:play club捏脸数据 编辑:程序博客网 时间:2024/04/29 10:08

目录

7.1。概观
7.2。检测到的错误:POSIX pthread API的滥用
7.3。检测到的错误:锁定排序不一致
7.4。检测到的错误:数据竞赛
7.4.1。简单数据竞赛
7.4.2。Helgrind的种族检测算法
7.4.3。解释竞赛错误讯息
7.5。有效使用Helgrind的提示和提示
7.6。Helgrind命令行选项
7.7。Helgrind监视器命令
7.8。Helgrind客户端请求
7.9。Helgrind的待办事项清单

要使用此工具,必须--tool=helgrind在Valgrind命令行上指定 。

7.1。概观

Helgrind是一个Valgrind工具,用于检测使用POSIX pthreads线程图元的C,C ++和Fortran程序中的同步错误。

POSIX pthreads中的主要抽象是:一组共享公共地址空间,线程创建,线程加入,线程退出,互斥(锁),条件变量(线程间通知),读写器锁,自旋锁,信号量的线程和障碍。

Helgrind可以检测三类错误,这些错误在接下来的三个部分将详细讨论:

  1. POSIX pthreads API的滥用。

  2. 锁定订购问题引起的潜在死锁。

  3. 数据竞赛 - 无需适当锁定或同步即可访问内存。

像这样的问题常常导致不可再现的,与时间相关的崩溃,僵局和其他不当行为,并且可能难以通过其他方式找到。

Helgrind知道所有的pthread抽象,并尽可能准确地跟踪它们的效果。在x86和amd64平台上,它了解并部分处理使用LOCK指令前缀引起的隐式锁定。在PowerPC / POWER和ARM平台上,它部分处理由加载链接和存储条件指令对引起的隐式锁定。

当您的应用程序仅使用POSIX pthreads API时,Helgrind效果最佳。但是,如果要使用自定义线程原语,可以使用定义的ANNOTATE_*宏来描述Helgrind的行为 helgrind.h

以下是 关于如何从Helgrind获得最佳效果的提示和提示的部分。

然后有一个命令行选项的 摘要。

最后, 对Helgrind可以改进的领域进行简要总结。

7.2。检测到的错误:POSIX pthread API的滥用

Helgrind拦截了许多POSIX pthread函数的调用,因此能够报告各种常见问题。尽管这些都是无人喜好的错误,但是他们的存在可能会导致程序行为的不确定性以及难以发现的错误。检测到的错误是:

  • 解锁无效互斥体

  • 解锁未锁定的互斥量

  • 解锁由不同线程持有的互斥体

  • 破坏无效或锁定的互斥体

  • 递归地锁定非递归互斥体

  • 释放包含锁定互斥体的内存

  • 将mutex参数传递给期望读写器锁参数的函数,反之亦然

  • 当POSIX pthread函数失败并且必须处理的错误代码时

  • 当线程仍然保持锁定锁定时退出

  • 调用pthread_cond_wait 与未锁定的互斥,无效的互斥体,或者一个由不同的线程锁定

  • 条件变量及其相关互斥体之间的绑定不一致

  • pthread屏障的无效或重复初始化

  • 线程仍在等待的pthread屏障的初始化

  • 破坏从未初始化的pthread屏障对象,或者线程仍在等待

  • 等待未初始化的pthread屏障

  • 对于Helgrind拦截的所有pthread函数,报告错误以及堆栈跟踪,如果系统线程库例程返回错误代码,即使Helgrind本身没有检测到错误

检查互斥体的有效性通常也适用于读写器锁。

还报告了各种这种不可能发生的事件。这些通常表示系统线程库中的错误。

报告的错误总是包含一个主堆栈跟踪,指示检测到错误的位置。它们还可能包含辅助堆叠轨迹,以提供其他信息。特别是与mutex相关的大多数错误也会告诉您互斥体最初来到Helgrind的注意事项(“ was first observed at”部分),因此您有机会找出它所指的互斥体。例如:

线程#1解锁了锁定在0x7FEFFFA90的锁定   在0x4C2408D:pthread_mutex_unlock(hg_intercepts.c:492)   通过0x40073A:近_main(tc09_bad_unlock.c:27)   由0x40079B:main(tc09_bad_unlock.c:50)  首先观察到锁定在0x7FEFFFA90   在0x4C25D01:pthread_mutex_init(hg_intercepts.c:326)   通过0x40071F:近_main(tc09_bad_unlock.c:23)   由0x40079B:main(tc09_bad_unlock.c:50)

Helgrind有一种总结线程身份的方法,正如您在这里看到的文本“ Thread #1”。这就是说,它可以谈论线程和线程,而不会压倒你的细节。有关 解释错误消息的更多信息,请参阅 下文。

7.3。检测到的错误:锁定排序不一致

在本节中,通常来说,“获取”锁只是意味着锁定该锁,并且“释放”锁定装置以将其解锁。

Helgrind监视线程获取锁定的顺序。这允许它检测可能由形成锁的周期而产生的潜在死锁。检测这种不一致是有用的,因为虽然实际的死锁相当明显,但在测试期间可能永远不会发现潜在的死锁,并且可能导致难以诊断的在役故障。

这样一个问题的最简单的例子如下。

  • 想象一下,一些共享资源R,无论什么原因,都被两个锁L1和L2保护,这两个锁必须在访问R时都被保持。

  • 假设一个线程获取L1,然后是L2,然后继续访问R.这意味着程序中的所有线程都必须按照L1和L2的顺序获取两个锁。不这样做会危害到僵局。

  • 如果两个线程(称为T1和T2)都要访问R,则会发生死锁。假设T1首先获取L1,并且T2首先获取L2。然后T1尝试获取L2,T2尝试获取L1,但是这些锁都已经被保持。所以T1和T2变得僵局。

Helgrind建立了一个有针对性的图表,指示过去获取锁的顺序。当一个线程获取一个新的锁时,图表被更新,然后检查它是否现在包含一个循环。循环的存在表明在循环中涉及锁的潜在的死锁。

一般来说,Helgrind将选择两个涉及循环的锁,并向您展示他们的采购订单是如何变得不一致的。它通过显示首先定义排序的程序点和稍后违反的程序点来实现。这是一个简单的例子,只涉及两个锁:

线程#1:锁定顺序“0x7FF0006D0在0x7FF0006A0之前”被违反观察(不正确)的顺序是:获取锁定在0x7FF0006A0   在0x4C2BC62:pthread_mutex_lock(hg_intercepts.c:494)   0x400825:main(tc13_laog1.c:23) 其次是以后获取锁定在0x7FF0006D0   在0x4C2BC62:pthread_mutex_lock(hg_intercepts.c:494)   通过0x400853:main(tc13_laog1.c:24)通过获取锁定在0x7FF0006D0建立所需的顺序   在0x4C2BC62:pthread_mutex_lock(hg_intercepts.c:494)   通过0x40076D:main(tc13_laog1.c:17) 随后稍后获取锁定在0x7FF0006A0   在0x4C2BC62:pthread_mutex_lock(hg_intercepts.c:494)   由0x40079B:main(tc13_laog1.c:18)

当循环中有两个以上的锁时,错误同样严重。然而,目前,Helgrind没有显示所涉及到的锁,有时是因为该信息不可用,而且也避免了信息的泛滥。例如,着名的“餐饮哲学家”问题的天真实施涉及五个锁的循环(见helgrind/tests/tc14_laog_dinphils.c)。在这种情况下,Helgrind已经发现,所有5位哲学家都可以同时拿起左叉,然后在等待拾取正确的叉子时死锁。

线程#6:锁定顺序“0x8049A00之前0x8049A00”被违反观察(不正确)的顺序是:获取锁定在0x8049A00   在0x40085BC:pthread_mutex_lock(hg_intercepts.c:495)   by 0x80485B4:dine(tc14_laog_dinphils.c:18)   由0x400BDA4:mythread_wrapper(hg_intercepts.c:219)   by 0x39B924:start_thread(pthread_create.c:297)   通过0x2F107D:clone(clone.S:130) 其次是以后获得锁定在0x80499A0   在0x40085BC:pthread_mutex_lock(hg_intercepts.c:495)   by 0x80485CD:dine(tc14_laog_dinphils.c:19)   由0x400BDA4:mythread_wrapper(hg_intercepts.c:219)   by 0x39B924:start_thread(pthread_create.c:297)   通过0x2F107D:clone(clone.S:130)

7.4。检测到的错误:数据竞赛

当两个线程访问共享内存位置,而不使用合适的锁或其他同步来确保单线程访问时,数据竞赛发生或可能发生。这种丢失的锁定可能导致模糊的与时间相关的错误。确保计划无竞争力是线程编程的核心难题之一。

可靠地检测种族是一个困难的问题,Helgrind的大部分内部都专门处理它。我们从一个简单的例子开始。

7.4.1。简单数据竞赛

关于比赛的最简单的例子如下。在这个程序中,不可能知道var在程序结束时的价值是什么。是2吗 还是1?

#include <pthread.h>int var = 0;void * child_fn(void * arg){   VAR ++; / *相对于父对象没有保护* / / *这是第6行* /   返回NULL;}int main(void){   pthread_t小孩   pthread_create(&child,NULL,child_fn,NULL);   VAR ++; / *相对于小孩没有保护* / / *这是第13行* /   pthread_join(child,NULL);   返回0;}

问题是var两个线程同时停止更新。正确的程序将var使用锁类型进行 保护pthread_mutex_t,这是在每次访问之前获取的,然后释放。Helgrind的这个程序的输出是:

线程#1是程序的根线程序线程#2已创建   在0x511C08E:克隆(在/lib64/libc-2.8.so)   由0x4E333A4:do_clone(在/lib64/libpthread-2.8.so)   by 0x4E33A30:pthread_create @@ GLIBC_2.2.5(在/lib64/libpthread-2.8.so中)   通过0x4C299D4:pthread_create @ *(hg_intercepts.c:214)   由0x400605:main(simple_race.c:12)线程#1在0x601038读取大小4期间的可能数据竞争锁定举行:无   在0x400606:main(simple_race.c:13)这与以前由线程#2写入的大小4冲突锁定举行:无   在0x4005DC:child_fn(simple_race.c:6)   by 0x4C29AFF:mythread_wrapper(hg_intercepts.c:194)   通过0x4E3403F:start_thread(在/lib64/libpthread-2.8.so)   由0x511C0CC:克隆(在/lib64/libc-2.8.so)位置0x601038是全局var“var”内的0个字节在simple_race.c中声明:3

这是一个非常简单的错误的细节。最后一个子句是主错误消息。它说,由于读取大小为4(字节),0x601038,这是var在程序main中第13行功能发生的地址,导致竞争。

消息的两个重要部分是:

  • Helgrind显示错误的两个堆栈跟踪,而不是一个。根据定义,比赛涉及两个访问相同位置的不同线程,结果取决于两个线程的相对速度。

    第一个堆栈跟踪文本“ Possible data race during read of size 4 ...”,第二个跟踪跟随文本“ This conflicts with a previous write of size 4 ...”。Helgrind通常能够显示比赛中涉及的两个访问。其中至少有一个将是一个写入(因为两个并发的,不同步的读取是无害的),并且它们当然是来自不同的线程。

    通过在两个位置检查您的程序,您应该能够至少了解问题的根本原因。对于每个位置,Helgrind显示在访问时保持的一组锁。这通常会使得明确哪个线程(如果有的话)无法执行所需的锁定。在这个例子中,两个线程都不会在访问期间保持锁定。

  • 对于在全局或堆栈变量上发生的种族,Helgrind会尝试识别变量的名称和定义点。因此文本“ Location 0x601038 is 0 bytes inside global var "var" declared at simple_race.c:3”。

    一旦Helgrind的程序启动并运行,显示堆栈和全局变量的名称就不会运行时间。但是,它需要Helgrind在程序启动时花费相当多的时间和内存来读取相关的调试信息。因此,默认情况下禁用此功能。要启用它,您需要--read-var-info=yes选择Helgrind。

以下部分更详细地说明了Helgrind的种族检测算法。

7.4.2。Helgrind的种族检测算法

大多数程序员根据线程库(POSIX Pthreads)提供的基本功能(线程创建,线程加入,锁定,条件变量,信号量和障碍)来考虑线程编程。

使用这些功能的效果是对存储器访问可能发生的顺序施加约束。这个暗示的顺序通常被称为“发生之前的关系”。一旦了解了之前的关系,很容易看出Helgrind如何在您的代码中找到种族。幸运的是,之前的关系本身很容易理解,本身就是推理并行程序行为的有用工具。我们现在简单介绍一下这个例子。

首先考虑以下buggy程序:

父线程:子线程:int var;//创建子线程在pthread_create(...)                          var = 20; var = 10;                                       出口//等孩子在pthread_join(...)printf(“%d \ n”,var);

父线程创建一个小孩。然后两者都将不同的值写入一些变量var,然后父级等待孩子退出。

var程序结束时的价值是10或20?我们不知道 该程序被认为是错误的(它有竞争),因为最终的值var取决于父和子线程的进度相对速率。如果父母很快,孩子很慢,孩子的任务可能会在以后发生,所以最终的值将是10; 如果孩子比父母快,反之亦然。

父母与孩子的相对进度不是程序员可以控制的,而且经常会从跑步转为跑步。这取决于诸如机器上的负载,还有什么运行,内核调度策略等许多因素。

明显的修复是使用锁来保护var。然而,有意义的是考虑一个更抽象的解决方案,即将消息从一个线程发送到另一个线程:

父线程:子线程:int var;//创建子线程在pthread_create(...)                          var = 20;//发送消息给孩子                                       //等待消息到达                                       var = 10;                                       出口//等孩子在pthread_join(...)printf(“%d \ n”,var);

现在程序可靠地打印“10”,不管线程的速度如何。为什么?因为孩子的作业在收到消息后才能发生。并且在父母的分配完成之后,消息不会被发送。

消息传输在两个作业之间创建一个“发生之前”依赖关系:var = 20; 必须在之前发生var = 10;。所以不再有比赛了var

请注意,父级向孩子发送消息并不重要。从小孩发送消息(在分配后)发送给父母(在分配之前)也会解决问题,导致程序可靠地打印“20”。

Helgrind的算法(在概念上)非常简单。它监视对内存位置的所有访问。如果一个位置(在这个例子中 var)被两个不同的线程访问,Helgrind会检查两次访问是否被发生在之前的关系中排序。如果是这样,那没关系 如果没有,它报告比赛。

重要的是要明白,发生之前的关系只会产生部分排序,而不是总排序。的总体排序的一个例子是数字的比较:对于任何两个数字 x和 y,或者 x是小于,等于或大于 y。部分排序就像一个完整的顺序,但它也可以表达两个元素既不相等,更少或更大但仅相对于彼此无序的概念。

在上面的固定例子中,我们说 var = 20;“发生在前” var = 10;。但在原始版本中,它们是无序的:我们不能说在任何情况下都发生。

说来自不同线程的两次访问是通过发生之前的关系来排序的?这意味着有一些线程间同步操作链,导致这些访问以特定顺序发生,而不考虑单个线程的实际进度。这是一个可靠的线程程序的必需属性,这就是Helgrind检查它的原因。

由标准线程图元创建的之前的关系如下:

  • 当线程T1和稍后(或立即)被线程T2锁定的互斥锁解锁时,在解锁之前,T1中的存储器访问必须在获取锁定之后在T2中进行。

  • 同样的想法适用于读写器锁,尽管有一些复杂性,以便允许读写对写入的正确处理。

  • 当条件变量(CV)由线程T1发信号通知并且其他线程T2由于相同CV的等待而被释放时,则信令之前的T1中的存储器访问必须发生 - 在T2中的存储器返回之后这段等待。如果没有线程等待CV,那么没有任何效果。

  • 如果在CV上广播T1,则所有等待的线程,而不是仅仅其中一个线程在广播线路上获得广播线程的先天依赖。

  • 在线程T1发布的信号量上完成sem_wait之后继续的线程T2获取对发布线程的依赖性,有点像依赖关系导致互斥锁解锁对。然而,由于信号量可以多次发布,所以在依赖性之前,等待呼叫中哪个发起呼叫发生的时候是未指定的。

  • 对于一组线程T1 .. Tn到达屏障然后移动,呼叫之后的每个线程在屏障之前的所有线程发生依赖之后。

  • 新创建的子线程在其父级创建点的时候获取一个初始的依赖关系。也就是说,在创建子进程之前,父进程执行的所有内存访问都将被视为发生在子进程的所有访问之前。

  • 类似地,当一个退出的线程通过调用获得pthread_join时,一旦调用返回,收获线程获得相对于由退出线程所做的所有存储器访问的先后依存关系。

总而言之:Helgrind拦截上述事件,并构建一个有针对性的非循环图,表示集体发生之前的依赖关系。它还监视所有内存访问。

如果一个位置被两个不同的线程访问,但是Helgrind没有找到任何路径,通过发生在之前的图形从一个访问到另一个访问,那么它报告比赛。

有几个注意事项:

  • 在两次访问都是读取的情况下,Helgrind不会检查一场比赛。这是愚蠢的,因为并发读取是无害的。

  • 即使通过任意长的同步事件链,两个访问也被认为是由先发生的依赖关系排序的。例如,如果T1访问某个位置L,然后pthread_cond_signalsT2(后来 pthread_cond_signalsT3接着访问L),则在第一次和第二次访问之间存在合适的先后依赖关系,即使它涉及两个不同的线程间同步事件。

7.4.3。解释竞赛错误讯息

Helgrind的种族检测算法收集了大量信息,并在检测到比赛时尝试以有用的方式呈现。以下是一个例子:

线程#2已创建   在0x511C08E:克隆(在/lib64/libc-2.8.so)   由0x4E333A4:do_clone(在/lib64/libpthread-2.8.so)   by 0x4E33A30:pthread_create @@ GLIBC_2.2.5(在/lib64/libpthread-2.8.so中)   通过0x4C299D4:pthread_create @ *(hg_intercepts.c:214)   通过0x4008F2:main(tc21_pthonce.c:86)线程#3已创建   在0x511C08E:克隆(在/lib64/libc-2.8.so)   由0x4E333A4:do_clone(在/lib64/libpthread-2.8.so)   by 0x4E33A30:pthread_create @@ GLIBC_2.2.5(在/lib64/libpthread-2.8.so中)   通过0x4C299D4:pthread_create @ *(hg_intercepts.c:214)   通过0x4008F2:main(tc21_pthonce.c:86)线程#3在0x601070读取大小4的可能数据竞争锁定举行:无   在0x40087A:child(tc21_pthonce.c:74)   by 0x4C29AFF:mythread_wrapper(hg_intercepts.c:194)   通过0x4E3403F:start_thread(在/lib64/libpthread-2.8.so)   由0x511C0CC:克隆(在/lib64/libc-2.8.so)这与以前由线程#2写入的大小4冲突锁定举行:无   在0x400883:child(tc21_pthonce.c:74)   by 0x4C29AFF:mythread_wrapper(hg_intercepts.c:194)   通过0x4E3403F:start_thread(在/lib64/libpthread-2.8.so)   由0x511C0CC:克隆(在/lib64/libc-2.8.so)位置0x601070是局部var“unprotected2”内的0字节在tc21_pthonce.c中声明:51,在线程3的框架#0中

Helgrind首先宣布错误消息中引用的任何线程的创建点。这样就可以简单地讲一下线程,而不会重复打印它们的创建点调用堆栈。每个线程只有一次宣布,首次出现在任何Helgrind错误消息中。

主要错误信息从文本“ Possible data race during read” 开始。一开始就是您期望看到的信息 - 赛车访问的地址和大小,无论是读取还是写入,以及在检测到点时的调用堆栈。

第二个调用堆栈从文本“ This conflicts with a previous write” 开始呈现。这显示了先前的访问,也访问了所述的地址,并被认为是与第一个调用堆栈中的访问竞争。请注意,此第二个调用堆栈限制为最多8个条目以限制内存使用。

最后,Helgrind可能会以源代码级别的方式来尝试描述一下比赛地址。在此示例中,它将其标识为局部变量,显示其名称,声明点,以及它居住在哪个第一个调用栈的框架中。请注意,此信息仅--read-var-info=yes 在命令行上指定时显示。这是因为读取DWARF3调试信息足够详细,以捕获变量类型和位置信息,使Helgrind在启动时更慢,并且对于大型程序也需要大量的内存。

一旦你有两个电话堆栈,你如何找到比赛的根本原因?

首先要检查每个调用堆栈引用的源位置。它们都应该显示对相同位置或变量的访问。

现在弄清楚该位置应该如何线程安全:

  • 也许这个位置是由互斥体保护的?如果是这样,您需要在两个接入点锁定和解锁互斥锁,即使其中一个访问被报告为读取。你有没有忘记在一个或另外的访问锁定?为了帮助您这样做,Helgrind显示每个线程在访问比赛位置时所持有的一组锁。

  • 或者,也许您打算使用其他一些方案来使其安全,例如在条件变量上发信号。在所有这种情况下,尝试找到一个同步事件(或其一个链),它将较早观察到的访问(如第二个调用堆栈所示)与稍后观察到的访问(如第一个调用堆栈所示)分离。换句话说,尝试找到证据表明早期访问“发生在之前”的后期访问。有关发生之前关系的说明,请参阅上一小节。

    Helgrind正在报道一场比赛的事实意味着在两次访问之间没有发生任何关系。如果Helgrind正常工作,那么即使仔细检查源代码,您也可能找不到任何此类关系。但是,希望您对代码的检查将显示丢失的同步操作应该在何处。

7.5。有效使用Helgrind的提示和提示

Helgrind可以非常有助于找到和解决与线程相关的问题。像所有复杂的工具一样,当您了解如何发挥其优势时,效果最好。

当您仅仅将一个现有的线程程序抛在脑后,Helgrind的效率将会降低,并且尝试了解任何报告的错误。如果您从一开始就设计线程程序,以帮助Helgrind验证正确性,那将会更有效。使用Memcheck查找内存错误也是如此,但在这里应用更多,因为线程检查是一个更难的问题。因此,编写一个正确的程序要比Helcred错误地报告(线程)错误要写正确的程序更容易,Memcheck错误地报告(内存)错误。

考虑到这一点,这里有一些提示,最重要的是列出最可靠的结果并避免错误的错误。前两个是至关重要的。任何违反他们的行为都将使您大量虚假的数据竞赛错误。

  1. 确保您的应用程序及其使用的所有库都使用POSIX线程原语。Helgrind需要能够看到与线程创建,退出,锁定和其他同步事件有关的所有事件。为此,它拦截了许多POSIX pthreads函数。

    不要将自己的线程原语(mutexes等)从Linux futex系统调用,原子计数器等的组合中滚出来。这些引发了Helgrind内部的不断变化的模式,并会给出虚假的结果。

    另外,不要使用其他POSIX抽象来重新实现现有的POSIX抽象。例如,不要从POSIX互斥体和条件变量构建自己的信号量例程或读写器锁。因为Helgrind直接支持它们,而是直接使用POSIX读写器锁和信号量。

    Helgrind直接支持以下POSIX线程抽象:互斥体,读写器锁,条件变量(见下文),信号量和障碍。目前,螺旋锁不支持,尽管它们可能在将来。

    在撰写本文时,以下流行的Linux软件包已知可以实现自己的线程图元:

    • Qt版本4.X. Qt 3.X是无害的,因为它只使用POSIX pthread的原语。不幸的是,Qt 4.X有自己的互斥体(QMutex)和线程捕获的实现。Helgrind 3.4.x包含对Qt 4.X线程的直接支持,这是实验性的,但被认为工作得很好。直接支持Qt 4的副作用是Helgrind可用于调试KDE4应用程序。由于这是一个实验性的功能,我们特别希望有人使用Helgrind来成功调试Qt 4和/或KDE4应用程序的反馈。

    • GNU OpenMP(GCC的一部分)的运行时支持库,至少对于GCC版本4.2和4.3。GNU OpenMP运行时库(libgomp.so)使用原子存储器指令和futex系统调用的组合构建自己的同步原语,这导致了Helgrind之后的混乱,因为它不能“看到”这些。

      幸运的是,这可以使用配置时选项(GCC)来解决。从源代码重建GCC,并配置使用 --disable-linux-futex。这使得libgomp.so代替使用标准的POSIX线程原语。请注意,这是使用GCC 4.2.3进行测试,并没有使用更近的GCC版本进行重新测试。我们很高兴听到有关更新版本的任何成功或失败。

    如果您必须实现自己的线程图元,那么有一组客户端请求宏helgrind.h来帮助您将其原始图形描述为Helgrind。您应该可以毫不费力地标记互斥体,条件变量等。

    也可以使用和ANNOTATE_HAPPENS_BEFORE, ANNOTATE_HAPPENS_AFTER和 ANNOTATE_HAPPENS_BEFORE_FORGET_ALL宏来标记线程安全引用计数的影响 。使用原子递增/递减的引用计数变量的线程安全引用计数导致Helgrind问题,因为引用计数的一到零转换意味着访问线程具有关联资源(通常为C ++对象)的独占所有权,因此可以访问它(通常是运行它的析构函数)而不锁定。Helgrind不明白这一点,加标是避免误报的关键。

    以下是C ++中线程安全引用计数的建议指南。您只需要标记您的发布方法 - 减少引用计数的方法。给了这样一个类:

    MyClass类{   unsigned int mRefCount;   void Release(void){      unsigned int newCount = atomic_decrement(&mRefCount);      if(newCount == 0){         删除这个;      }   }}

    释放方法应标注如下:

       void Release(void){      unsigned int newCount = atomic_decrement(&mRefCount);      if(newCount == 0){         ANNOTATE_HAPPENS_AFTER(&mRefCount);         ANNOTATE_HAPPENS_BEFORE_FORGET_ALL(&mRefCount);         删除这个;      } else {         ANNOTATE_HAPPENS_BEFORE(&mRefCount);      }   }

    这个方案有一些复杂的,大多数是理论上的反对意见。从理论的角度来看,似乎不可能制定一个完全正确的标记方案,即保证消除所有的虚假种族的意义。所提出的方案在实践中表现良好。

  2. 避免记忆回收。如果你不能避免它,你必须使用告诉Helgrind通过 VALGRIND_HG_CLEAN_MEMORY客户端请求(in helgrind.h)发生了什么。

    Helgrind知道通过mallocfreenewdelete 和堆栈帧的进入和退出发生的标准堆内存分配和释放 。特别是当记忆通过freedelete或功能退出释放时,Helgrind认为记忆清洁,所以当它最终被重新分配时,它的历史是无关紧要的。

    然而,通常的做法是实现内存回收方案。在这些中,要释放的内存不会传递给 freedelete,而是放入一个可用缓冲区的池中,以便根据需要再次发出。问题是Helgrind无法知道这样的记忆在逻辑上不再被使用,它的历史是无关紧要的。因此,您必须使用明确的方式,使用VALGRIND_HG_CLEAN_MEMORY客户端请求来指定相关的地址范围。将这些请求放入池管理器代码是最简单的方法,当内存返回到池中或从中分配时使用它们。

  3. 避免POSIX条件变量。如果可以,使用POSIX信号量(sem_tsem_post, sem_wait)做线程间的事件信号。初始值为零的信号量对此尤其有用。

    Helgrind仅部分正确处理POSIX条件变量。这是因为Helgrind 只有在等待的线程首先到达会合(才能实际调用)的时候才能看到pthread_cond_wait调用和 pthread_cond_signal/ / pthread_cond_broadcast调用 之间的线程间依赖关系pthread_cond_wait。如果信号器首先到达,则不能看到线程之间的依赖关系。在后一种情况下,POSIX指南意味着相关联的布尔状态仍然提供线程间同步事件,但Helgrind不可见的同步事件。

    Helgrind缺少一些线程间同步事件的结果是使其报告误报。

    这种同步损失的根本原因尤其难以理解,所以一个例子是有帮助的。已经由Arndt Muehlenfeld(“多线程程序运行时候赛检测”,论文,奥地利TU格拉茨)进行了讨论。条件变量的规范POSIX推荐使用方案如下:

    b是布尔条件,大部分时间是Falsecv是条件变量mx是其相关的互斥体信使者:服务员:锁(mx)锁(mx)b = True while(b == False)信号(cv)等待(cv,mx)解锁(mx)解锁(mx)

    假设b大部分时间是假的。如果服务员首先到达会合点,则进入其同步循环,等待信号器发出信号,并最终继续进行。Helgrind看到信号,注意到依赖,一切都很好。

    如果b信号器首先到达,则设置为true,并且信号消失在无处。当服务员稍后到达时,它不进入其循环,只是继续进行。但即使在这种情况下,跟踪while循环后的服务器代码也不能执行,直到信号器设置b为True。因此,仍然有相同的线程间依赖性,但是这次通过任意的内存条件,Helgrind看不到它。

    相比之下,Helgrind对由信号量操作引起的线程间依赖性的检测被认为是完全正确的。

    据我所知,这个问题的解决方案不需要条件变量等待循环的源代码级注解超出了现有技术。

  4. 确保您正在使用受支持的Linux发行版。目前,Helgrind只能正确支持glibc-2.3或更高版本。这反过来意味着我们只支持glibc的NPTL线程实现。旧的LinuxThreads实现不受支持。

  5. 如果您的应用程序使用线程局部变量,helgrind可能会报告这些变量的错误的正面竞争条件,尽管很可能是免费的。在Linux上,您可以使用--sim-hints=deactivate-pthread-stack-cache-via-hack 以避免这种假阳性错误消息(请参阅--sim-hints)。

  6. 使用完成所有线程 pthread_join。避免分离线程:不要在分离状态下创建线程,并且不要调用pthread_detach现有线程。

    使用pthread_join完成的线程可以提供Helgrind和程序员可以看到的清晰的同步点。如果您不调用 pthread_join线程,Helgrind相对于程序中其他线程的任何重要同步点,无法知道何时完成。因此,它假定线程无限期地停留,并且可能无限期地与程序的存储器状态干涉。它有权假设 - 毕竟,由于安排原因,退出的线程在其生命的最后阶段确实运行得非常缓慢。

  7. 一起执行线程调试(使用Helgrind)和内存调试(使用Memcheck)。

    Helgrind详细跟踪内存状态,应用程序中的内存管理错误可能会导致混乱。在极端情况下,已知有许多无效读取和写入(特别是释放内存)的应用程序会使Helgrind崩溃。因此,理想情况下,您应该在使用Helgrind之前使您的应用程序Memcheck-clean。

    除非您首先删除线程错误,否则可能无法使您的应用程序Memcheck-clean。特别地,在程序终止时,可能难以删除在多线程C ++析构函数序列中释放内存的所有读写操作。所以,理想情况下,在使用Memcheck之前,应该使您的应用程序Helgrind-clean。

    由于这个圆形显然是无法解决的,至少要记住,Memcheck和Helgrind在一定程度上是相辅相成的,你可能需要一起使用它们。

  8. POSIX要求的标准I / O(的实现printffprintf, fwritefread,等)是线程安全的。不幸的是,GNU libc通过使用Helgrind无法拦截的内部锁定原语来实现。因此,当您使用这些功能时,Helgrind会产生许多虚假的竞赛报告。

    Helgrind尝试使用标准的Valgrind错误抑制机制来隐藏这些错误。所以,至少对于简单的测试用例,你看不到任何的。然而,有些可能会滑过。只是一些要注意的事情。

  9. Helgrind的错误检查在系统线程库本身(libpthread.so)中无法正常工作,并且通常会在其中观察到大量(false)错误。Valgrind的抑制系统然后过滤掉这些,所以你不应该看到它们。

    如果你看到报道任何地方比赛失误libpthread.so或者 ld.so是最里面的堆栈帧关联的对象,请在提交错误报告 http://www.valgrind.org/。

7.6。Helgrind命令行选项

以下最终用户选项可用:

--free-is-write=no|yes [default: no]

启用时(不是默认值),Helgrind处理释放堆内存,就好像内存是在空闲之前写的。这暴露了一个线程引用内存的种族,并被另一个线程释放,但没有可观察到的同步事件,以确保引用在免费之前发生。

这个功能是Valgrind 3.7.0中的新功能,被认为是实验性的。默认情况下不启用它,因为它与自定义内存分配器的交互目前还不太了解。欢迎用户反馈。

--track-lockorders=no|yes [default: yes]

启用(默认)时,Helgrind执行锁定顺序一致性检查。对于一些错误的程序,报告的大量锁定顺序错误可能会变得烦人,特别是如果您只对种族错误感兴趣。因此,您可能会发现禁用锁定顺序检查有用。

--history-level=none|approx|full [default: full]

--history-level=full(默认)使Helgrind收集有关“旧”访问的足够信息,它可以在竞争报告中生成两个堆栈跟踪 - 当前访问的堆栈跟踪以及较旧的冲突访问的跟踪。为了限制内存使用,“旧”访问堆栈跟踪最多限制为8个条目,即使 --num-callers值较大。

收集这些信息在速度和存储器中都是昂贵的,特别是对于执行许多线程间同步事件(锁定,解锁等)的程序而言。没有这样的信息,更难以追查种族的根本原因。尽管如此,您可能不需要它,只需要检查是否存在种族,例如在进行以前无竞争程序的回归测试时。

--history-level=none是相反的极端。它使Helgrind不会收集有关以前访问的任何信息。这可以快得多--history-level=full

--history-level=approx在这两个极端之间提供了妥协。它会导致Helgrind显示后续访问的完整跟踪信息,并且大概有关早期访问的信息。这个近似信息由两个堆栈组成,较早的访问保证发生在由两个堆栈表示的程序点之间的某处。这不如显示以前访问的确切堆栈(同样--history-level=full),但它比没有更好,它几乎一样快 --history-level=none

--conflict-cache-size=N [default: 1000000]

这个标志只能有效果--history-level=full

关于“旧”冲突访问的信息存储在具有LRU风格管理的有限大小的高速缓存中。这是必要的,因为存储由程序进行的每个单个存储器访问的堆栈跟踪是不实际的。定期丢弃不近期访问的位置的历史信息,以释放高速缓存中的空间。

该选项根据存储有冲突的访问信息的不同内存地址的数量来控制缓存的大小。如果您发现Helgrind仅使用一个堆栈而不是预期的两个堆栈显示种族错误,请尝试增加此值。

最小值为10,000,最大值为30,000,000(默认值的三十倍)。将值增加1将Helgrind的内存需求提高大约100个字节,因此最大值将容易地占用三个额外的千兆字节内存。

--check-stack-refs=no|yes [default: yes]

默认情况下,Helgrind会检查您的程序进行的所有数据存储器访问。该标志允许您跳过检查对线程堆栈(局部变量)的访问。这可以提高性能,但是以堆栈分配的数据丢失竞赛为代价。

--ignore-thread-creation=<yes|no> [default: no]

控制线程创建过程中是否应忽略所有活动。默认情况下仅在Solaris上启用。Solaris提供了比其他操作系统更高的吞吐量,并行性和可扩展性,代价是更细粒度的锁定活动。这意味着例如,当在glibc下创建一个线程时,所有线程设置只使用一个大锁。Solaris libc使用几个细粒度的锁,并且创建者线程尽快恢复其活动,例如堆栈和TLS设置顺序到创建的线程。这种情况使Helgrind感到困惑,因为它假定在创建者和创建的线程之间存在一些错误的顺序; 因此,不会报告申请中的许多类型的种族条件。yes为了防止这种错误排序, 在Solaris上默认情况下将此命令行选项设置为。所有活动(加载,存储,客户端请求)因此在以下期间被忽略:

  • pthread_create()在创建者线程中调用

  • 线程创建阶段(堆栈和TLS设置)在创建的线程中

在线程创建期间分配的新内存也将被追溯,那就是竞赛报告被抑制。DRD隐含地做同样的事情。这是必要的,因为Solaris libc缓存了许多对象,并为不同的线程重用它们,并且使Helgrind混淆。

7.7。Helgrind监视器命令

Helgrind工具提供由Valgrind内置的gdbserver 处理的监视器命令(请参阅Valgrind gdbserver的Monitor命令处理)。

  • info locks [lock_addr]显示锁的列表及其状态。如果 lock_addr给出,只显示位于该地址的锁。

    在以下示例中,helgrind知道一个锁。此锁位于访客地址ga 0x8049a20。锁类型表示rdwr 读写器锁。其他可能的锁类型是nonRec(简单互斥体,非递归)和mbRec(简单互斥体,可能是递归的)。然后锁类型后面是保存锁的线程列表。在下面的示例中,R1:thread #6 tid 3 表示helgrind线程#6已经获取(一旦作为字母R之后的计数器为1)锁定在读取模式。helgrind线程nr为每个启动的线程递增。“tid 3”的存在表示线程#6还没有退出,并且是valgrind tid 3.如果一个线程已经终止,那么用“tid(退出)”指示。

    (gdb)监视器信息锁锁ga 0x8049a20 {   亲切的rdwr {R1:thread#6 tid 3}}(GDB) 

    如果您提供该选项--read-var-info=yes,则将提供有关锁定位置的更多信息,例如包含该锁的全局变量或堆块:

    锁ga 0x8049a20 { 位置0x8049a20是全局var“s_rwlock”内的0个字节 在rwlock_race.c中声明:17   亲切的rdwr {R1:thread#3 tid 3}}
  • accesshistory <addr> [<len>] 显示从<addr>开始的<len>(默认为1)字节记录的访问历史记录。对于与给定范围重叠的每个记录的访问,accesshistory显示操作类型(读取或写入),读取或写入的地址和大小,执行操作的helgrind线程nr / valgrind tid号码以及由线程持有的锁定操作时间。首先显示最早的访问权限,最近的访问权限显示在最后。

    在下面的例子中,我们首先看到已经修改了给定的2个字节范围的线程#7记录了4个字节的写入。第二个记录的写入是最近记录的写入:线程#9修改了相同的2个字节作为4字节写入操作的一部分。还会显示每个线程在写操作时保持的锁的列表。

    (gdb)monitor accesshistory 0x8049D8A 2通过线程#7写入大小为4的0x8049D88 tid 3== 6319 ==锁定:2,地址0x8049D8C(和1,无法显示)== 6319 == at 0x804865F:child_fn1(locked_vs_unlocked2.c:29)== 6319 == 0x400AE61:mythread_wrapper(hg_intercepts.c:234)== 6319 == by 0x39B924:start_thread(pthread_create.c:297)== 6319 == by 0x2F107D:clone(clone.S:130)通过线程#9写入大小为4的0x8049D88 tid 2== 6319 ==锁定:2,地址0x8049DA4 0x8049DD4== 6319 == at 0x804877B:child_fn2(locked_vs_unlocked2.c:45)== 6319 == 0x400AE61:mythread_wrapper(hg_intercepts.c:234)== 6319 == by 0x39B924:start_thread(pthread_create.c:297)== 6319 == by 0x2F107D:clone(clone.S:130)

7.8。Helgrind客户端请求

下面定义了以下客户端请求 helgrind.h。看到这个文件的参数的确切细节。

  • VALGRIND_HG_CLEAN_MEMORY

    这使得Helgrind忘记了关于指定内存范围的所有内容。这对于希望回收内存的内存分配器特别有用。

  • ANNOTATE_HAPPENS_BEFORE

  • ANNOTATE_HAPPENS_AFTER

  • ANNOTATE_NEW_MEMORY

  • ANNOTATE_RWLOCK_CREATE

  • ANNOTATE_RWLOCK_DESTROY

  • ANNOTATE_RWLOCK_ACQUIRED

  • ANNOTATE_RWLOCK_RELEASED

    这些用于向Helgrind描述自定义(非POSIX)同步原语的行为,否则无法理解。请参阅helgrind.h进一步文档的注释。

7.9。Helgrind的待办事项清单

以下是一些松散的结束列表,应该整理一下。

  • 对于锁定顺序错误,打印完整的锁定循环,而不是仅在目前的2个周期内执行。

  • 有冲突的访问机制有时神秘地不能显示冲突的访问“堆栈,即使提供有无冲突的访问信息的无界存储。这应该进行调查。

  • 由GCC为投机商店创建的线程不安全代码引起的文档竞赛。在临时看 http://gcc.gnu.org/ml/gcc/2007-10/msg00266.html http://lkml.org/lkml/2007/10/24/673

  • 不要更新锁定顺序图,并且不要检查是否出现“尝试”状态的锁定操作时的错误(例如 pthread_mutex_trylock)。这样的呼叫不会对锁定顺序添加任何实际的限制,因为它们总是无法获取锁定,导致呼叫者离开并执行计划B(大概它将有一个计划B)。进行这样的检查可能会产生错误的锁定顺序错误并混淆用户。

  • 表现可能非常差 100:1的减速并不罕见。性能改进的范围有限。

原创粉丝点击