##线程本地存储 TLS(静态TLS)
TLS的概念,以下链接讲的比较详细
静态TLS
上面链接中提到__thread关键字是GCC编译器的扩展,并不属于标准,所以在window下的VC++是没有这个关键字的。不过VC++也提供一个对应的关键字 __declspec来声明线程变量。如下:
__declspec( thread ) int tls_i = 1;
- __thread 只能用于修辞POD类型,不能修饰class类型,因为无法自动调用构造函数和析构函数。
- 在VC++下,__declspec 可以用于修饰对象,能调用对象的构造函数和析构函数,如下示例代码:
#include <thread>
class T
{
public:
T():v(0)
{
std::cout << "create t" << std::endl;
}
~T()
{
std::cout << "delete t" << std::endl;
}
void PrintValue()
{
std::cout << "=========> v:"<< v << std::endl;
}
void SetValue(int i)
{
v = i;
}
private:
int v;
};
__declspec(thread) T t1;
void test()
{
t1.PrintValue();
}
void test1()
{
t1.SetValue(16);
}
int main()
{
std::thread tt1(test1);
std::thread tt2(test);
system("pause");
}
线程结束时会分别调用对象的构造及析构函数
windows下TSL 详细见如下链接:
https://msdn.microsoft.com/en-us/library/6yh4a9k1.aspx
https://msdn.microsoft.com/en-us/library/windows/desktop/ms686749(v=vs.85).aspx
https://msdn.microsoft.com/en-us/library/ms686991(v=vs.85).aspx
C++11提供了thread_local关键字实现了线程存储
https://stackoverflow.com/questions/6021273/how-to-allocate-thread-local-storage
http://en.cppreference.com/w/cpp/language/storage_duration
TLS的应用
在muduo的日志库中作者提到了几处优化的地方,其中有两条就是通过TLS来实现的,我觉得很有借鉴意义。
- 对日志时间戳的缓存:每一条日志的时间戳格式:20180101 13:00:12 123221。那么在一秒内输出多条日志时,日期和时间是不变的,变的是微秒。那么可以将格式化的时间字符串及精度为秒的时间值缓存起来。只有在跨秒时再重新格式化时间字符串。对于每一条日志,产生该日志的肯定是一个确定线程,对于格式化的时间字符串等变量对该线程来说就是线程私有的。那么对用于缓存的变量是可以用__thread声明的, 一是提高执行效率,而是不用考虑互斥的问题。
__thread char t_time[32];
__thread time_t t_lastSecond;
t_time[32] 是用来缓存为格式后的时间戳字符串,精度是到秒。其被声明为__thread
t_lastSecond 用存储上一秒的时间戳,精度为秒。其被声明为__thread
以下是muduo日志库中时间戳生成的代码
void Logger::Impl::formatTime()
{
int64_t microSecondsSinceEpoch = time_.microSecondsSinceEpoch();
time_t seconds = static_cast<time_t>(microSecondsSinceEpoch / 1000000);
int microseconds = static_cast<int>(microSecondsSinceEpoch % 1000000);
if (seconds != t_lastSecond)
{
t_lastSecond = seconds;
struct tm tm_time;
::gmtime_r(&seconds, &tm_time);
int len = snprintf(t_time, sizeof(t_time), "%4d%02d%02d %02d:%02d:%02d",
tm_time.tm_year + 1900, tm_time.tm_mon + 1, tm_time.tm_mday,
tm_time.tm_hour, tm_time.tm_min, tm_time.tm_sec);
assert(len == 17); (void)len;
}
//微秒值是单独生成的
Fmt us(".%06dZ ", microseconds);
assert(us.length() == 9);
stream_ << T(t_time, 17) << T(us.data(), 9);
}
对线程ID的缓存,每个线程的ID都是唯一的,私有的。所以非常适合用__thread修辞。在muduo库中有对thread id的缓存及对格式化的缓存。定义在CrrentThread命名空间中,都用 __thread修辞。代码如下:
namespace CurrentThread
{
// internal
extern __thread int t_cachedTid;
extern __thread char t_tidString[32];
extern __thread const char* t_threadName;
void cacheTid();
inline int tid()
{
if (t_cachedTid == 0)
{
cacheTid();
}
return t_cachedTid;
}
inline const char* tidString() // for logging
{
return t_tidString;
}
inline const char* name()
{
return t_threadName;
}
bool isMainThread();
}
//初始化
namespace CurrentThread
{
__thread int t_cachedTid = 0;
__thread char t_tidString[32];
__thread const char* t_threadName = "unknown";
const bool sameType = boost::is_same<int, pid_t>::value;
BOOST_STATIC_ASSERT(sameType);
}
//缓存thread id
void CurrentThread::cacheTid()
{
if (t_cachedTid == 0)
{
t_cachedTid = detail::gettid();
int n = snprintf(t_tidString, sizeof t_tidString, "%5d ", t_cachedTid);
assert(n == 6); (void) n;
}
}
在muduo的日志库中,每条消息都会记录产生该消息的对应的thread id,所以将thread id及格式化的字符串缓存起来也可以提高日志性能。
总结
归纳一下TLS使用的场景:
- 线程私有的对象,如文件fd,socket链接。计数,缓存等。
- 还可以修饰那些”值可能会变,带有全局性,但是又不值得用全局锁保护“的变量。