在 catch { ... } 中使用 bthread 后,exception 对象会提前析构
- 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
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