apache / apache/brpc

在 catch { ... } 中使用 bthread 后,exception 对象会提前析构

Open
#2,278 2 comments 0 reactions 0 assignees View on GitHub
usage
Dominant language
C++
Stars
17.6k
Forks
4.1k
Avg merge
2d 12h
Merged PRs (30d)
69

Description

**Describe the bug (描述bug)**
C++ 异常机制倚赖 TLS 实现。若在 catch 代码块中让 bthread 被换出,则有小概率导致 exception 对象被提前析构。

例如在下面的代码中

```
try {
...
} catch (exception& e) {
Foo(); // 该函数是一个阻塞的函数,导致当前 bthread 被换出。
std::cout << e.msg << std::endl; // 小概率出错
}
```

详细的解释请看下面复现的测试用例。

**To Reproduce (复现方法)**

下面这个用例可以复现此问题。

```
#include
#include
#include
#include
#include

namespace brpc_test
{

struct MyException
{
bthread_t bthread_id;

MyException()
{
printf("[%lu] %s %p\n", bthread_self(), __FUNCTION__, this);
bthread_id = bthread_self();
}

~MyException()
{
printf("[%lu] %s %p\n", bthread_self(), __FUNCTION__, this);
bthread_id = 0;
}
};

static void* MyBthread(void *unused)
{
printf("[%lu] Enter %s\n", bthread_self(), __FUNCTION__);
try
{
printf("[%lu] Throw\n", bthread_self());
throw MyException();
}
catch (const MyException& e)
{
printf("[%lu] Catch %p\n", bthread_self(), &e);
EXPECT_EQ(bthread_self(), e.bthread_id);
printf("[%lu] Sleep\n", bthread_self());
bthread_usleep(1); // 让当前协程切换出去
printf("[%lu] Resume\n", bthread_self());
EXPECT_EQ(bthread_self(), e.bthread_id); // ERROR
}
printf("[%lu] Leave %s\n", bthread_self(), __FUNCTION__);
return NULL;
}

TEST(BugDemo, Brpc)
{
// 直接使用 bthread_start_background() 是多线程的,不容易复现 bug。
// 我们定义一个单线程的线程池,
bthread::TaskControl tc;
EXPECT_EQ(0, tc.init(1));

bthread::TaskGroup *tg = tc.choose_one_group();
bthread_t thread1;
tg->start_background(&thread1, NULL, MyBthread, NULL);
bthread_t thread2;
tg->start_background(&thread2, NULL, MyBthread, NULL);

bthread_join(thread1, NULL);
bthread_join(thread2, NULL);
tc.stop_and_join();
}

} // namespace brpc_test
```

这个程序的输出为如下(我加了点注释)。

```
Note: Google Test filter = BugDemo.Brpc
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from BugDemo
[ RUN ] BugDemo.Brpc
[4294967552] Enter MyBthread # bthread1 开始执行
[4294967552] Throw
[4294967552] MyException 0x7f72dc01fa30 # bthread1 分配 MyException 对象 e1
[4294967552] Catch 0x7f72dc01fa30
[4294967552] Sleep # bhread1 换出
[4294967553] Enter MyBthread # bthread2 开始执行
[4294967553] Throw
[4294967553] MyException 0x7f72dc0263c0 # bthread2 分配 MyException 对象 e2
[4294967553] Catch 0x7f72dc0263c0
[4294967553] Sleep # bthread2 换出
[4294967552] Resume # bthread1 换入
[4294967552] ~MyException 0x7f72dc0263c0 # bthread1 析构 e2,注意这里开始出问题了!!
[4294967552] Leave MyBthread
[4294967553] Resume # bthread2 换入
/mag/workspace/bug-demo/brpc_test.cpp:42: Failure # bthread2 访问到已析构的对象 e2
Expected equality of these values:
bthread_self()
Which is: 4294967553
e.bthread_id
Which is: 0
[4294967553] ~MyException 0x7f72dc01fa30 # bthread2 析构 e1
[4294967553] Leave MyBthread
[ FAILED ] BugDemo.Brpc (3 ms)
[----------] 1 test from BugDemo (3 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (3 ms total)
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] BugDemo.Brpc
```

**Expected behavior (期望行为)**

**Versions (各种版本)**
OS: Linux 3.10
Compiler: gcc 9.2.1
brpc: 1.5.0
protobuf: n/a

**Additional context/screenshots (更多上下文/截图)**

Contributor guide

Open the contributing guide

Research direction

The reproduction is the BugDemo.Brpc test in brpc_test.cpp and uses bthread/bthread.h, task_control.h, and task_group.h. Start by running that test on the stated Linux/GCC setup, then trace exception lifetime across bthread_usleep and task switching; done means the test passes without either coroutine observing a destructed exception.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.