用户注册的bthread local变量析构函数触发bthread切换引起coredump
Describe the bug (描述bug)
问题1
使用了bthread local会以一定概率出现coredump,并且core的栈不固定,但大多在bthread::TaskGroup::sched_to的 errno = saved_errno;,分析后发现是由于注册的bthread local析构函数调用了bthread_mutex_lock会引发bthread local析构的时候出发bthread切换导致的,如下

- 假设bthread1在pthread1上调用return_keytable最终会调用用户注册的bthread local析构函数,如果这个析构函数调用了bthread_mutex_lock等会导致bthread切换的函数则会使得执行return_keytable之后bthread1切换到另一个线程pthread2上
- 由于g还是用的pthread1上的thread local tls_task_group,在pthread2调用g->set_remained(TaskGroup::_release_last_context, m);会使得pthread1提前将bthread1的stack释放掉,如果此时另一个bthread2申请到了这个stack则会导致bthread1和bthread2共用了同一个stack,会导致互相踩栈引发coredump
问题1修复
可以移动g = tls_task_group到bthread local析构函数之后的位置修复
问题2
应用上述修复后会遇到第二个问题,是由于gcc编译优化thread local访问方式结合bthread跨线程调度导致的
bthread meta中记录的local_storage.keytable是正确的但是tls_bls.keytable被设置为null导致一个运行过程中的bthread突然获取不到这个bthread之前注册过的bthread local变量引起coredump



可以看到编译优化后将tls_bls的地址保存在了寄存器r15中
我们知道如果用户注册的bthread local析构函数如果存在引起bthread切换的调用,return_keytable可能会触发bthread从pthread1切换到pthread2上,这里由于gcc将tls_bls的地址保存在r15寄存器的优化会导致在pthread2执行 tls_bls.keytable = NULL; 这个操作实际上是将pthread1的tls_bls.keytable设置为null了,而此时pthread1上运行的bthread2可能设置过tls_bls.keytable是不为null的,也就导致了我们上面看到的现象,pthread1上的bthread2获取不到自己设置的bthread local变量了
问题2修复
这个问题使用原子变量或memory fence也解决不了,因为这两者对gcc来说只能组织缓存值的优化不能阻止缓存地址的优化 可以将 KeyTable* kt = tls_bls.keytable; 修改为 KeyTable* kt = m->local_storage.keytable 判定上是等价的,但是可以先避开gcc 缓存tls_bls地址的优化
也可以考虑将thread local关键字声明的tls_bls用pthread_getspecific函数api替代,因为gcc无法对通过pthread_getspecific获取到的thread local做缓存优化,不过这样改的会有点多
To Reproduce (复现方法)
自己写一个注册bthread local 析构函数中调用bthread_mutex_lock的代码多线程并发启动,停止bthread以触发bthread_local析构
Expected behavior (期望行为)
Versions (各种版本) OS: Compiler: gcc 4.8.5 brpc: v1.0.0 protobuf:
Additional context/screenshots (更多上下文/截图)