[paper]Eraser: A Dynamic Data Race Detector for Multithreaded Programs

Eraser原理

最初的思路

为每个共享变量维护一个lockset,记录线程访问这个变量时持有的锁。每次访问会更新这个lockset(与当前持有的锁做交运算),当这个集合为空时,就说明这个变量没有被锁保护。

改进一:初始化和共享读

  • 初始化时可以不加锁,因为此时别的线程根本没有机会访问到这个变量,因此将refine lockset的时间调整到初始化之后。定义初始化结束的时间为该变量被另一个线程访问。
    • 如果一个线程还没初始化完成,就错误地将这个变量暴露给了外部,没法保证这种错误一定被检测到,这依赖于其他线程是否在初始化完成之前访问了这个变量
  • 一个只读变量是可以不加锁的,因此需要延后报告错误的时间,直到一个初始化后的变量被多个线程之后才报错。

Eraser状态转换图

改进二:读写锁

Read-Write Lock管理一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。

将线程持有的锁(交叉)存在两个集合中:

  • locks_held(t)中存的是任何状态下t持有的锁
  • write_locks_held(t)中存的是写状态下t持有的锁

在refine lockset的交运算中,如果是读操作,目标集合是locks_held;如果是写操作,目标集合是write_locks_held。

这样一来,一次写操作可以将多次读操作的锁在lockset中删去,本来这样的锁也没有保护写的功能。

实现

  • 堆上的或全局的32-bit word被看作一个共享变量

  • Lockset原理图

    Lockset-mechanical

  • 误报的情况

    • 内存重复利用
    • 使用自己实现的锁,而不是使用pthreads
    • 良性竞争

    这些可以通过特殊的标记语句消除。

  • 内核中经常使用的IRQL提升也有锁的功能,假设提升到level n,则视为申请了前n个锁,这样就可以处理lock和IRQL共存的代码。

  • 使用P/V机制可能会导致误报,因为这种机制基于信号量(Semaphore),没法得知它保护哪一个变量。

和论文没啥直接关系但很有用的一些基础知识

  • data race发生的条件

    • 两个线程至少有一个是写线程
    • 没有指明同时访问的机制
  • 中断请求级别(IRQL, Interrupt ReQuest Level):处理器在一个IRQL上执行线程代码。IRQL是帮助决定线程如何被中断的。在同一处理器上,线程只能被更高级别IRQL的线程中断。

总结

为每个变量维护一个lockset的思路很不错,效果也很好。正如作者最后说的,Eraser也有其局限:比如没法处理多个锁的情况、没法处理P/V机制的锁等等、误报率不低,有时不得不修改源码(添加标记)。