ThreadSanitizer检测工具-动态数据竞争检测技术

ThreadSanitizer是检测数据争用的工具。它由一个编译器检测模块和一个运行时库组成。Thread Sanitizer会记录每一个内存访问的信息,并检测该访问是否参与了竞争。
Data Race
Data Race是指多个线程在没有正确加锁的情况下,同时访问同一块数据,并且至少有一个线程是写操作,对数据的读取和修改产生了竞争,从而导致各种不可预计的问题。

Data Race的问题非常难查,Data Race一旦发生,结果是不可预期的,也许直接就Crash了,也许导致执行流程错乱了,也许把内存破坏导致之后某个时刻突然Crash了。
在纯Happens-before模式下会漏掉很多竞争情况,在混合模式下有存在太多的噪音。因此到了2008年底,我们就实现一个自己的竞争检测器,我们称之为“ThreadSanitizer”。
它采用了一种新的混合算法,可以方便地运行在纯Happens-before模式下,它也支持动态的annotations。同时我们让竞争报告尽可能地易读。

ThreadSanitizer采用的是动态检测技术,它不会扫描分析源代码,而是以程序运行中产生的一系列离散的事件点为输入,进行分析,从而找到竞争。最重要的事件就是内存访问和同步。

内存访问即Read和Write,同步事件则要么是锁事件要么是Happens-before事件。
锁事件又分为WrLock,RdLock,WrUnLock,RdUnLock。
Happens-before事件则分为Signal和Wait。这些事件由运行程序产生,经由底层binary translation框架(Valgrind)的帮助交给ThreadSanitizer。

在进行进一步的介绍之前,我们需要先来认识两个竞争检测算法:LockSet-Based与Happens-before。
以下内容来自:Hybrid Dynamic Data Race Detection这篇文章。
LockSet-Based竞争检测
基于这样一个假设:无论何时只要两个不同的线程访问了相同的共享内存地址,并且其中一个为写操作,那么这两次访问操作必须要持有某一个共同的锁。
当该假设被违反时,我们就视为具有潜在的竞争情况。可以用如下公式来描述:
Happens-before竞争检测
ppens-before关系可以定义如下:

  1. 两个事件ei,ej如果同属于一个线程,并且ei发生在ej之前,那么我们就说
    
  2. i->j (i happen before j)

  3. 如果ei是消息g的发送方,ej是消息g的接收方,那么我们也说i->j
    

Happens-before具有传递性,即:i->j & j->k可以推出i->k。

实际上,happens-before竞争检测也是非常简单的:如果我们观察到两个事件ei和ej,它们访问了相同的内存位置,同时至少一个是Write操作,同时既没有i->j,也没有j->i,我们就认为存在潜在的竞争。
也就是说如果两个线程访问了同一个内存地址,但是这两次访问之间又没有Lamport定义的那种happens-before关系,我们就认为它们之间存在潜在的竞争。
实际上只要是它报告的竞争都是说明实现上存在可以改进的线程调度问题,但是另一方面它存在更多的漏报。
当然作为动态检测技术,这两种方法都存在漏报,因为它们的检测依赖于实际的程序执行过程。

简单来说:锁和Happens-before都是保证了两个线程不会同时访问同一内存地址。锁通过互斥保证,Happens-before则建立了不同线程中事件先后的偏序关系。没有这些就意味着存在潜在竞争。
Lockset based要求更严,因此存在误报,但是不会漏报。Happens-before要求更松,因此存在漏报,但是不会误报**
两者优势互补,因此后面就有了基于二者的混合模式:锁用Lockset-Based,不再为Unlock/lock建立happen before关系,剩下的再用happens-before。但是这种模式仍然存在误报
另外在理解竞争检测算法时,还是要从各个线程发生的事件来考虑,而不是从线程的实现代码上来考虑。也就是说在每一次检测中,每个线程都对应了一系列前后发生的事件,然后去判断这些事件间的happens-before关系(或lockset存在交集),在那些没有此类关系的事件中,如果访问了相同的内存区域,那么就说明存在竞争。
例子一:**

#include <pthread.h>
int Global;
void *Thread1(void *x) {
  Global = 42;
  return x;
}
int main() {
  pthread_t t;
  pthread_create(&t, NULL, Thread1, NULL);
  Global = 43;
  pthread_join(t, NULL);
  return Global;
}

编译:$ clang -fsanitize=thread -g -O1 tiny_race.c
如果检测到错误,程序将向stderr打印一条错误消息。当前,ThreadSanitizer使用外部addr2line过程来象征其输出 (以后将予以修复)
在这里插入图片描述
该报告包含数据竞争的内存访问的说明。对于这两种访问,它都会说出内存地址(您可以与日志/调试器匹配),大小以及是否是读或写的。
请注意,第一个内存访问是“当前”访问,即检测到竞争的访问。而第二次访问是先前的一些内存访问。
共享内存地址:0x0000014b3c60
对于这两种访问,报告都会说出线程ID,并显示线程创建堆栈(主线程除外)。
如果竞争发生在堆内存位置上,则报告还包含分配位置和堆块的参数:
https://github.com/google/sanitizers/wiki/ThreadSanitizerReportFormat
例子二:
$ cat simple_race.cc

#include <pthread.h>
#include <stdio.h>
 
int Global;
 
void *Thread1(void *x) {
  Global++;
  return NULL;
}
 
void *Thread2(void *x) {
  Global--;
  return NULL;
}
 
int main() {
  pthread_t t[2];
  pthread_create(&t[0], NULL, Thread1, NULL);
  pthread_create(&t[1], NULL, Thread2, NULL);
  pthread_join(t[0], NULL);
  pthread_join(t[1], NULL);
}

上面的代码在不加锁的情况下,两个线程同时去修改Global变量,从而导致Data Race。使用gcc的-fsanitize=thread 编译,执行
clang -fsanitize=thread -g -O1 simple_race.cc
在这里插入图片描述

执行程序,如果发生Data Race,错误信息会直接输出出来。如果错误信息比较多,重定向输出流到文件里,慢慢分析:

./a.out >result.txt 2>&1在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值