Callstack markers

有时,在程序中需要保存函数的上下文信息,比如某一个函数是否在执行,或者函数的调用链是怎样的。

本文尝试着解决这类问题。

问题的提出

首先,假定我们有一个线程安全的队列类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Thread safe queue (Multi-producer/Multi-consumer)
template <typename T>
struct ThreadSafeQueue {
public:
// Add a new element
void push(T v);

// Retrieves an element from the queue, optionally blocking to wait for new
// elements if the queue is empty
// \return True if an element was retrieved, false if 'cancel' was called
bool pop(T& v, bool wait);

// Causes any current or future calls to 'pop' to finish with false
void cancel();
};

然后我们以此构建了一个异步任务队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class WorkQueue {
public:
void post(std::function<void()> w) {
m_work.push(w);
}
// Run enqueued handlers. "wait" causes it to keep processing work until
// "stop" is called
void run(bool wait) {
std::function<void()> w;
while (m_work.pop(w, wait)) {
w();
}
}
// Stops processing new work (causing "run" to return)
void stop() {
m_work.cancel();
}

// Some method that let us know if we are currently executing our run
// method
bool isInRun();

private:
// Some thread safe queue...
ThreadSafeQueue<std::function<void()>> m_work;
};

现在,我们有一个函数,它的执行流程取决于当前线程是否正在执行isInRun()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Some global work queue
WorkQueue globalWork;

void func1() {
// Code needs to behave differently if executing "run" on the globalWork
// instance at the moment
if (globalWork.isInRun()) {
// ...
} else {
// ..
}
}

int main() {
globalWork.post([]() { func1(); });
globalWork.run(false); // Execute any currently enqueued handlers
return 0;
}

问题来了,我们应该如何实现WorkQueue::isInRun()方法呢?

朴素办法

也许我们可以通过一个成员变量来告诉我们是否正在执行WorkQueue::isInRun()方法,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
class WorkQueue {
public:
void run(bool wait) {
m_running++;
// ...
m_running--;
}

bool isInRun() const { return m_running > 0; }
private:
int m_running = 0;
};

为什么不用一个bool类型的m_running呢?主要是为了判断递归(recursion)。

为什么run会出现递归的情况呢?因为它内部调用了用户提交的各类任务(handlers),这些任务的实现(implement)中可能调用任何代码,其中就包括run

另外值得强调的是,在调用这种未知的代码时,程序内部一定不能持有上锁了的互斥量,否则很容易造成死锁(deadlocks)。

这种朴素的办法可行吗?答案是否定的。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// This works correctly
void test1() {
globalWork.post([] {
func1(); // isInRun() returns true here
});
// isInRun() returns false here
func1();

// Explicitly run any handlers
globalWork.run(false);
}

// This doesn't work correctly
void test2() {
// Create worker thread to process work
auto th = std::thread([] { globalWork.run(true); });
globalWork.post([] {
// isInRun() returns true here
func1();
});

// WRONG behaviour: isInRun() returns true here.
// Even worse, it might return true or false depending if the worker thread
// is already running or not
func1();

// shutdown worker thread
globalWork.stop();
th.join();
}

更适用的办法

为了解决上述的问题,我们试图通过记录每个线程的函数调用栈来判断某一个函数是否正在执行。

具体来说,我们会在函数调用中逐层放置标记,每一个标记会连接到前一层调用的标记。如果我们将标记设为thread local,那我们可以为每一个线程创建一个可以迭代的调用标记。

callstack

参考boost::asio::detail::call_stack的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
template <typename Key, typename Value = unsigned char>
class Callstack {
public:
class Iterator;

class Context {
public:
Context(const Context&) = delete;
Context& operator=(const Context&) = delete;
explicit Context(Key* k)
: m_key(k), m_next(Callstack<Key, Value>::ms_top) {
m_val = reinterpret_cast<unsigned char*>(this);
Callstack<Key, Value>::ms_top = this;
}

Context(Key* k, Value& v)
: m_key(k), m_val(&v), m_next(Callstack<Key, Value>::ms_top) {
Callstack<Key, Value>::ms_top = this;
}

~Context() {
Callstack<Key, Value>::ms_top = m_next;
}

Key* getKey() {
return m_key;
}

Value* getValue() {
return m_val;
}

private:
friend class Callstack<Key, Value>;
friend class Callstack<Key, Value>::Iterator;
Key* m_key;
Value* m_val;
Context* m_next;
};

class Iterator {
public:
Iterator(Context* ctx) : m_ctx(ctx) {}
Iterator& operator++() {
if (m_ctx)
m_ctx = m_ctx->m_next;
return *this;
}

bool operator!=(const Iterator& other) {
return m_ctx != other.m_ctx;
}

Context* operator*() {
return m_ctx;
}

private:
Context* m_ctx;
};

// Determine if the specified owner is on the stack
// \return
// The address of the value if present, nullptr if not present
static Value* contains(const Key* k) {
Context* elem = ms_top;
while (elem) {
if (elem->m_key == k)
return elem->m_val;
elem = elem->m_next;
}
return nullptr;
}

static Iterator begin() {
return Iterator(ms_top);
}

static Iterator end() {
return Iterator(nullptr);
}

private:
static thread_local Context* ms_top;
};

template <typename Key, typename Value>
typename thread_local Callstack<Key, Value>::Context*
Callstack<Key, Value>::ms_top = nullptr;

这种模板类的实现意味着下面这些使用均可行:

1
2
3
4
Callstack<Foo>
Callstack<Foo, int>
Callstack<Bar>
Callstack<Bar,SomeData>

这种实现的优点如下:

  • 类型安全。你可以指定任意的键值Key/Value类型。
  • 每一种键值类型均代表了独立的链表。实际使用中有选择地具现化模板可以在不损失性能的情况下自由地使用。
  • 将调用栈的逻辑和具体的类型解耦。换句话说,不再需要之前列出的WorkQueue::isInRun方法了。
  • 其他功能。这个辅助类不仅适用于函数调用栈的检查,还可以用于调试。举例来说,通过给标记附加调试信息,当程序检测到错误时,它可以遍历标记然后打印所有的调用和调试信息。

使用示例

先来看一下如何解决上述提出的WorkQueue的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Foo {
template <typename F>
void run(F f) {
// Place marker in the callstack, so any called code knows we are
// executing "run" on this Foo instance
Callstack<Foo>::Context ctx(this);
f();
}
};

Foo globalFoo;

void func1() {
printf("%s\n", Callstack<Foo>::contains(&globalFoo) ? "true" : "false");
}

int main() {
func1(); // Will print "false"
globalFoo.run(func1); // Will print "true"
return 0;
}

再看一下如何用它来打印调试信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
struct DebugInfo {
int line;
const char* func;
std::string extra;

template <typename... Args>
DebugInfo(int line, const char* func, const char* fmt, Args... args)
: line(line), func(func) {
char buf[256];
sprintf(buf, fmt, args...);
extra = buf;
}
};

#ifdef NDEBUG
#define MARKER(fmt, ...) ((void)0)
#else
#define MARKER(fmt, ...) \
DebugInfo dbgInfo(__LINE__, __FUNCTION__, fmt, __VA_ARGS__); \
Callstack<DebugInfo>::Context dbgCtx(&dbgInfo);
#endif

void fatalError() {
printf("Something really bad happened.\n");
printf("What we were doing...\n");
for (auto ctx : Callstack<DebugInfo>())
printf("%s: %s\n", ctx->getKey()->func, ctx->getKey()->extra.c_str());
exit(1); // Kill the process
}

void func2(int a) {
MARKER("Doing something really useful with %d", a);
// .. Insert lots of useful code ...

// Something went wrong, lets trigger a fatal error
fatalError();
}

void func1(int a, const char* b) {
MARKER("Doing something really useful with %d,%s", a, b);
// .. Insert lots of useful code ...
func2(100);
}

int main() {
func1(1, "Crazygaze");
return 0;
}

调试信息不仅可以是函数的调用栈(LINE/FUNCTION),你还可以添加其他的附加信息,方便Debug。

在下一篇文章中,我们会使用这个Callstack类来实现我们自己的boost::asio::io_service::strand