ThreadSanitizer应该是google三年前开发出来的,主要用于检测C/C++中的数据竞争漏洞,但一直没有进行更新(支持clang3.2和gcc4.8),据师傅说里面的模型不实用,不过通过github上的ThreadSanitizer文档来认识一下竞争漏洞,还是挺不错的。
数据竞争指的是两个线程并发访问了一个变量,并且其中一个是写操作。例如,以下代码可能引发crash和内存错误:
#include
#include
#include
#include
typedef std::map<std::string, std::string> map_t;
void *threadfunc(void *p) {
map_t& m = *(map_t*)p;
m["foo"] = "bar";
return 0;
}
int main() {
map_t m;
pthread_t t;
pthread_create(&t, 0, threadfunc, &m);
printf("foo=%s\n", m["foo"].c_str());
pthread_join(t, 0);
}
两个线程访问同一全局变量,没有采取同步操作。通常,这种竞争也是良性的,例如有些计数变量不要求太精确。但有些变量很关键,例如对钱计数的变量。
int var;
void Thread1() { // Runs in one thread.
var++;
}
void Thread2() { // Runs in another thread.
var++;
}
引用计数,竞争可能引起内存泄露或double free。实例可见http://crbug.com/15577。
// Ref() and Unref() may be called from several threads.
// Last Unref() destroys the object.
class RefCountedObject {
...
public:
void Ref() {
ref_++; // Bug!
}
void Unref() {
if (--ref_ == 0) // Bug! Need to use atomic decrement!
delete this;
}
private:
int ref_;
};
两个线程访问同一个复杂对象(例如STL 容器,c++标准模板库),而没有进行同步操作。
std::map<int,int> m;
void Thread1() {
m[123] = 1;
}
void Thread2() {
m[345] = 0;
}
对于以下代码:
bool done = false;
void Thread1() {
while (!done) {
do_something_useful_in_a_loop_1();
}
do_thread1_cleanup();
}
void Thread2() {
do_something_useful_2();
done = true;
do_thread2_cleanup();
}
两个线程的同步是通过boolean变量done实现的,引发错误。
在x86上,编译器会对代码进行优化:一是do_something_useful_2()的部分代码会被挪到"done = true"之后;二是do_thread2_cleanup()部分代码会被挪到"done = true"之前;三是如果do_something_useful_in_a_loop_1()不修改"done",编译器会重写Thread1如下:
if (!done) {
while(true) {
do_something_useful_in_a_loop_1();
}
}
do_thread1_cleanup();
这样,Thread1就永远不会停止。
除了x86架构,cache或指令乱序执行会导致其他问题。
大多数动态竞争检测器会报告这类数据竞争,也即使用该bool变量进行同步(do_something_useful_2() 和 do_thread1_cleanup()之间)的情况。
某线程初始化一个对象指针(开始是null),另一个线程等待,直到对象指针非空。如果没有进行正确的同步操作,编译器会做些奇怪的转化,导致错误。除此以外,在某些架构上,这类竞争会因为cache导致错误。
MyType* obj = NULL;
void Thread1() {
obj = new MyType();
}
void Thread2() {
while(obj == NULL)
yield();
obj->DoSomething();
}
对象被构造两次,导致内存泄露。
static MyObj *obj = NULL;
void InitObj() {
if (!obj)
obj = new MyObj();
}
void Thread1() {
InitObj();
}
void Thread2() {
InitObj();
}
更新数据时采用读取锁。
void Thread1() {
mu.ReaderLock();
var++;
mu.ReaderUnlock();
}
void Thread2() {
mu.ReaderLock();
var++;
mu.ReaderUnlock();
}
以下代码初看是对的,但如果x是struct { int a:4, b:4; },就会有bug。怎么理解?
void Thread1() {
x.a++;
}
void Thread2() {
x.b++;
}
以下是反面教材,该错误还是很常见。重复检查锁,怎么理解?
bool inited = false;
void Init() {
// 可能被多个线程调用
if (!inited) {
mu.Lock();
if (!inited) {
// .. initialize something
}
inited = true;
mu.Unlock();
}
}
void Thread1() {
SomeType object;
ExecuteCallbackInThread2(
SomeCallback, &object);
...
// "object" is destroyed when
// leaving its scope.
}
虚函数知识可参考https://www.cnblogs.com/lifexy/p/10629539.html
假如你有以下结构:
struct A {
virtual ~A() {
F();
}
virtual void F() {
printf("In A");
}
};
struct B : public A {
virtual void F() {
printf("In B");
}
};
当释放B时,你会看到"In A"被打印,执行A::A()会释放A和所有以A为基类的对象(除了被B覆盖的析构函数)。为了实现该机制,gcc会在B::B()开头分配一个vptr指针(指向B::vtable虚函数表的指针),在A::~A()开头也分配指针指向A::vtable。
竞争:类A有函数Done()、虚函数F()和虚析构函数,析构函数等待Done()事件。类B继承A,并覆写了A::F()。
#include
#include
class A {
public:
A() : done_(false) {}
virtual void F() { printf("A::F\n"); }
void Done() {
std::unique_lock<std::mutex> lk(m_);
done_ = true;
cv_.notify_one();
}
virtual ~A() {
std::unique_lock<std::mutex> lk(m_);
cv_.wait(lk, [this] {return done_;});
}
private:
std::mutex m_;
std::condition_variable cv_;
bool done_;
};
class B : public A {
public:
virtual void F() { printf("B::F\n"); }
virtual ~B() {}
};
int main() {
A *a = new B;
std::thread t1([a] {a->F(); a->Done();});
std::thread t2([a] {delete a;});
t1.join(); t2.join();
}
创建对象a(静态类型A和动态类型B),第1个线程执行a->F(),然后给第2个线程发送信号;第2个线程调用delete a(B::B)实际上调用了A::A,A::A会等待第1个线程的信号。析构函数A::A覆盖了指向A::vptr的vptr,如果在第2个线程开始执行A::~A时,第1个线程执行a->F(),则A::F先被执行,而不会执行B::F。
解决办法:如果类包含虚函数,则不要在构造函数和析构函数中使用同步操作,用Start()和Join()方法。ThreadSanitizer可以区别vptr上的有害竞争。
这是vptr竞争的变种:
class Base {
Base() {
global_mutex.Lock();
global_list.push_back(this);
global_mutex.Unlock();
// point (A), see below
}
virtual void Execute() = 0;
...
};
class Derived : Base {
Derived() {
name_ = ...;
}
virtual void Execute() {
// use name_;
}
string name_;
...
};
Mutex global_mutex;
vector<Base*> global_list;
// Executed by a background thread.
void ForEach() {
global_mutex.Lock();
for (size_t i = 0; i < global_list.size(); i++)
global_list[i]->Execute()
global_mutex.Unlock();
}
看上去,global_list是用global_mutex正确同步了,但是加入到global_list的对象只有一部分被构造(也可以被其他线程访问)。例如,如果线程先被point(A)占用,ForEach()会触发虚函数调用。
有时一个线程在释放堆内存,另一个线程使用该内存。
int *array;
void Thread1() {
array[10]++;
}
void Thread2() {
free(array);
}
AddressSanitizer可以检测该漏洞。
如果在其他线程还在运行的时候调用exit,静态对象可能被多个线程同时释放和使用(所以尽量别使用静态non-POD对象)。
#include "pthread.h"
#include
static std::map<int, int> my_map;
void *MyThread(void *) { // runs in a separate thread.
int i = 0;
while(1) {
my_map[(i++) % 1000]++;
}
return NULL;
}
int main() {
pthread_t t;
pthread_create(&t, 0, MyThread, 0);
return 0;
// exit() is called, my_map is destructed
}
对mutex进行加锁和解锁操作是同步的,但mutex可能被不安全释放。
class Foo {
Mutex m_;
...
public:
// Asynchronous completion notification.
void OnDone(Callback *c) {
MutexLock l(&m_);
// handle completion
// ...
// schedule user completion callback
ThreadPool::Schedule(c);
}
...
};
void UserCompletionCallback(Foo *f) {
delete f; // don't need it anymore
// notify another thread about completion
// ...
}
本例中,如果UserCompletionCallback已经在另一个线程上执行,析构函数MutexLock可以解锁已经删除的mutex。
文件描述符内部是同步的,但是用户代码中对文件描述符错误的读写操作可能引发竞争。
int fd = open(...);
// Thread 1.
write(fd, ...);
// Thread 2.
close(fd);
fd描述符可能被释放后使用,此时fd可能是其他文件或socket,导致数据泄露。
以下是可检测的竞争案例,代码完整,可编译后研究。
ThreadSanitizerCppManual:https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual
ThreadSanitizerPopularDataRaces:https://github.com/google/sanitizers/wiki/ThreadSanitizerPopularDataRaces
ThreadSanitizerDetectableBugs:https://github.com/google/sanitizers/wiki/ThreadSanitizerDetectableBugs