一、线程局部变量
thread_local,线程局部存储变量。这个变量是通过TLS来实现的线程局部存储。在前面的分析中知道,它意味着每个线程有自己独立的变量副本。这比通过共享变量再使用同步机制进行控制的优势不言而喻。
在早期c++标准尚未正式推出线程局部存储变量时,在Windos平台和Linux平台下都有自己的相关处理机制。比如POSIX中的pthread_key_t系列应用。这些应用本质上都是相同的,底层的存储机制是和c++标准中的thread_local关键字的机制是一样的。当然,在细节的应用上,还是有着各自的特点,这才是标准的意义。
二、特点和优势
在前面的文章中对底层的内存分配机制进行过详细的说明和分析。其实在很多的内存分配器如jemalloc等的实现中,他们都会让线程独占本地缓存页,这样分配内存时直接从线程自身的的链表中进行操作(简单的指针移动)即可。这意味着,线程局部存储的处理不需要进行系统分配内存也不需要进行同步操作。它的处理效率和栈内存的分配差不了多少(其实也好理解,线程本身也是一个栈空间)。
它的优势的核心来源在于thread local,它没有内存共享而是多个同名的副本。线程各自操作自身的内存,不需要进行复杂的各种同步或者CAS等操作,就可以实现并行操作。
不过,正如前面反复提到的一个问题,优势在某些情况下,往往即是劣势。它既然是local,就有几个问题:
- 大小
线程自身的空间往往不大(虽然可以调整),一般到M级就已经是很大的了。所以,如果在thread_local使用一些容器或类似的变长空间操作,就需要严格的注意数据的大小,防止溢出。另外,即使不溢出的情况下,也要防止在常驻(如服务器)多个线程中容器的内存回收问题,防止一直占用导致整体的内存占用率过高 - 传递
既然是thread_local,它就意味着这个变量是和当前的线程绑定的。当这个线程存在时,这个变量是存在的。但是如果退出呢?所以,不要将thread_local变量传递给其它线程使用。如果出现这种情况,就意味着设计的失误 - 跨作用域调用
既然是本地的存储,既然和线程生命周期绑定。就不要去干涉外部的作用域内的变量操作,如分配、回收内存等等。否则的话,线程的任何问题,都可能引发不可预知的错误,直到崩溃。特别是在类对象的析构中更是如此 - 线程的伪共享
在前面专门对线程的伪共享进行过分析。所以一定要注意多个thread_local可能进入同一个Cache Line的情况。尽量使用内存对齐来防止这种现象的发生。虽然说这种现象可能非常少见。但如果出现,还真不好发现问题
可以这样说,想用好thread_local,就必须深入的掌握其内部的机理。不过对于大多数的开发者来说,应用的往往是一些很简单的场景,所以出现问题的可能性极低。但心里的那根弦,还是要崩着。防止出了问题都无处下手。
三、应用场景
这里给出几个具体的应用场景,不再象前面泛泛的说明:
- 压缩处理
压缩处理中,如果使用同一个压缩器实例,就会产生对冲突。而在常见的压缩算法中,可能往往会需要对相关的哈希结构进行处理,这时如果使用thread_local,优势就相当明显了,等于是线程各自控制自己的副本,大幅提高效率。这种现象的本质其实是thread_local的私有上下文的一种应用场景 - 日志缓冲区处理
日志输出往往是多线程的,但为了在效率和性能间平衡,往往需要分批写入,而不是有一条日志就写入。这样就可以在线程本地进行缓冲 - 多线程随机发生器
使用随机发生器,可以保证每个线程的随机种子都是安全的,随机值的分布也是可保障的 - 性能计时
这个和随机发生器有些类似,它可以防止对值的累加时需要同步的情况,实现一个本地的安全的计时器 - 小批量对象缓冲区复用
这个其日志有些类似,只不过它抽象到了对象层次。像一些对象的序列化或反序列化(如json),就可以直接在thread_local进行
其实通过上面的例子就可以简单总结一些类似的应用场景,如图片的分场处理,分段的数据查找等等。它都可以通过thread_local满足当前的应用。
四、例程
下面看一个简单的例子:
c
#include <atomic>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
thread_local int t_counter = 0;
std::mutex g_ioMutex;
std::atomic<int> g_total{0};
void worker(int id, int count) {
for (int i = 0; i < count; ++i) {
++t_counter;
}
g_total += t_counter;
std::lock_guard<std::mutex> lock(g_ioMutex);
std::cout << "thread " << id << " count = " << t_counter << "\n";
}
void incrementTwice() { t_counter += 2; }
void workerShared(int id) {
t_counter = 0; // 重置本线程计数器
incrementTwice(); // +2
incrementTwice(); // +2
++t_counter; // +1
g_total += t_counter;
std::lock_guard<std::mutex> lock(g_ioMutex);
std::cout << "thread " << id << " shared cuount = " << t_counter << "\n";
}
int main() {
std::vector<std::thread> threads;
threads.emplace_back(worker, 1, 100);
threads.emplace_back(worker, 2, 200);
threads.emplace_back(worker, 3, 300);
for (auto &t : threads) {
t.join();
}
threads.clear();
threads.emplace_back(workerShared, 4);
threads.emplace_back(workerShared, 5);
for (auto &t : threads) {
t.join();
}
t_counter = 50;
std::cout << "master t_counter = " << t_counter << "\n";
std::cout << "add sum = " << g_total.load() << "\n";
return 0;
}
只是一个计数器应用,非常简单,上机跑一下就都明白了。
五、总结
thread_local在实际的开发中,一般程序员用的不多。但在某些场景下,可能有意想不到的作用。但在使用它前,要仔细想一下,thread_local是否是必须的?是否真正提高了效率?是否完全可以被其它替代?只有明白这一系列的关键因素,并掌握前面的细节后,才能够真正的大胆应用thread_local。c++灵活性是给胆大心细的开发者提供锋利的刀,而不是给楞头青提供自杀的匕首。