问题出处

写C++出身的人, 很有可能写过下面这样的代码.

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
void set() {

{
// C++有一个特性, 块中的变量会在离开代码块时被析构
// 这里定义了一个l变量, l变量在此处被构造, 在当前的程序块结束时被析构.
// 用了它之后再也不用担心忘记调用mutex.unlock了.
Lock l(&s->delMtx);

// do whatever
}

return ;
}

class Lock
{
public:
explicit Lock(pthread_mutex_t *mx) : mutex(mx)
{
int err = pthread_mutex_lock(mutex); // 构造时加锁
printf("[LOCKER]: lock %d\n", err);
}
~Lock()
{
int err = pthread_mutex_unlock(mutex); // 析构时unlock
printf("[LOCKER]: unlock %d\n", err);
mutex = nullptr;
}

private:
Lock(const Lock &l);
pthread_mutex_t *mutex;
};

于是

今天我就在Go里面这么用了一次.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func newTTLMap() (m *TTLMap) {
m = &TTLMap{m: make(map[int]*item)}

go func() {
for ;; {

{ // 我为了缩小临界区, 特意只在这小块代码里加锁
log.Info("map lock")
m.mtx.Lock()
defer m.mtx.Unlock()
// for k, v := range m.m 遍历这个map
}
// Do something
}
}()
return m
}

而后调用程序, 还是发生了死锁, 原因就是上面的mtx调用了Lock()函数, 可是没有调用Unlock()函数, 第二次加锁的时候当然会发生死锁了.

再后来

真的, 自己不好好理解文档真的很心累. 文档中是这样说的

Go’s defer statement schedules a function call (the deferred function) to be run immediately before the function executing the defer returns. It’s an unusual but effective way to deal with situations such as resources that must be released regardless of which path a function takes to return. The canonical examples are unlocking a mutex or closing a file.

defer是调度一个function call的, 并不是一个code block, 是在函数返回前立即执行.

和代码块没有任何关系, 代码块里面调用了defer, 最终也是在函数结束前执行.

怎么办呢?

发现了这个问题, 我也很绝望啊, 之前的C++经验不如没有的好.

最后想了想, 有这么两种解决方案吧.

第一种(不用defer了, 我记性好, 不怕):

1
2
3
4
5
6
7
8

{ // 我为了缩小临界区, 特意只在这小块代码里加锁
m.mtx.Lock()

// for k, v := range m.m 遍历这个map

m.mtx.Unlock()
}

第二种(强行defer, 把它变成匿名函数):

1
2
3
4
5
6
func() {// 我为了缩小临界区, 特意只在这小块代码里加锁
m.mtx.Lock()
defer m.mtx.Unlock()

// for k, v := range m.m 遍历这个map
}()

不知道你们觉得哪种更好, 虽然临界区不可能很大, 忘记Unlock()还是挺难的. 但我觉得, 能交给机器去做的, 不要交给人了, 我偏爱第二种吧, 其实都可以的.

一点寄语

首先, 我承认自己没有好好理解Golang, 这是根本原因.

这个问题到底是怎么发现的: 最近写代码我决定不能随便干了, 于是今天写的时候 自己把单元测试也写了一点, 发现程序不自动结束, 进一步又发现锁一直没被释放, 这要真的全部集成好, 再调试错误, 那可真就完了.

希望大家引以为戒, 花点时间在单元测试上, 是有很大好处的. 早测试, 早享受.