1 thread-local问题
调用阻塞的bthread函数后,所在的pthread很可能改变,这使pthread_getspecific,gcc __thread和c++11 thread_local变量,pthread_self()等的值变化了,如下代码的行为是不可预计的:
thread_local SomeObject obj;
...
SomeObject* p = &obj;
p->bar();
bthread_usleep(1000);
p->bar();
bthread_usleep之后,该bthread很可能身处不同的pthread,这时p指向了之前pthread的thread_local变量,继续访问p的结果无法预计。这种使用模式往往发生在用户使用线程级变量传递业务变量的情况。为了防止这种情况,应该谨记:
- 不使用线程级变量传递业务数据。这是一种槽糕的设计模式,依赖线程级数据的函数也难以单测。判断是否滥用:如果不使用线程级变量,业务逻辑是否还能正常运行?线程级变量应只用作优化手段,使用过程中不应直接或间接调用任何可能阻塞的bthread函数。比如使用线程级变量的tcmalloc就不会和bthread有任何冲突。
- 如果一定要(在业务中)使用线程级变量,使用bthread_key_create和bthread_getspecific。
2 为什么使用线程级变量的tcmalloc就不会和bthread有任何冲突?
tcmalloc如何使用TLS:tcmalloc为每个线程(pthread)分配了一个本地的内存缓存(thread cache)。当线程需要分配小块内存时,它可以无锁地从自己的thread cache中获取,这极大地减少了线程间竞争全局内存分配锁的开销。当thread cache耗尽或清理时,它才会与全局的中心数据结构进行交互。
2.1 核心原理:tcmalloc的线程缓存是pthread级别
tcmalloc的高性能核心在于其线程缓存(Thread Cache) 机制。每个pthread都拥有自己独立的线程缓存,用于快速分配小对象(通常≤32KB)
- 分配动作:当你的代码在某个bthread中调用
malloc
或new
时,这个bthread正托管在一个具体的pthread(假设叫Pthread A)上。此时,tcmalloc会尝试从Pthread A的线程缓存(Thread Cache A) 中分配内存。如果Thread Cache A中有合适的空闲对象,分配会立即完成,且完全无锁。 - 释放动作:当你释放这块内存时,你的bthread可能已经因
bthread_usleep
等操作被调度到了另一个pthread(假设叫Pthread B)上。此时,free
或delete
操作会发生在了Pthread B上。tcmalloc会识别出这块内存最初是由哪个size class分配(它通过内存地址背后的元信息可以做到这一点),并将其归还到当前所在pthread(Pthread B)的线程缓存(Thread Cache B) 中对应的空闲链表里。
2.2 跨线程释放与中央堆(Central Heap)
你可能会问,在Pthread B上释放了原本属于Pthread A的内存,这会不会乱?短期来看,这确实会导致“非对称分配/释放”:
- Pthread A的线程缓存会因为频繁分配而更快地被耗尽,需要更频繁地从全局的中央堆(Central Heap) 申请新的内存块(这个过程需要加锁)。
- Pthread B的线程缓存则会因为接收了其他线程释放的内存而更快地达到阈值,需要将其返还给中央堆(同样需要加锁)。
这种跨线程的内存释放和回收,虽然可能带来一些性能开销(因为涉及中央堆的锁操作),但完全不会引发正确性问题。tcmalloc的中央堆是所有线程共享的全局资源,它确保了内存最终都能被正确回收和复用