简介:
对于一个存在多个线程的进程来说,有时需要每个线程都自己操作自己的这份数据。这有点类似c++类的实例的属性,每个实例对象操作的都是自己的属性。我们把这样的数据成为线程局部存储(thread local storage,TLS)
一、定义
- 线程局部存储是指对象内存在线程开始后分配,线程结束时回收,且每个线程有该对象自己的实例。
- 简单的说,线程局部存储的对象都是独立于各个线程的。
- 实际上,这不是一个新鲜的概念,虽然c++一直没有在语言层面支持它,但是很早之前操作系统就有办法执行线程局部存储了。(c++直到c++11才从语言层面实现了)
二、线程局部存储的实现
- 由于线程本身是操作系统中的概念,因此线程局部存储这个功能是离不开操作系统支持的。
- 而不同的操作系统对线程局部存储的实现也不相同,以至于使用的系统api也有区别,
- 这里我们对windows和linux简单介绍下,对c++11提供的线程局部存储我们详细写下demo。
1、windows系统
2、linux系统
3、c++11
三、windows系统
1、线程局部存储是分块的(TLS_MINIMUM_AVAILABLE)
- windows将线程局部存储区分成TLS_MINIMUM_AVAILABLE个块,每个块都通过1个索引值对外提供访问。
- TLS_MINIMUM_AVAILABLE默认是64,在winnt.h文件中有如下定义:
# define TLS_MINIMUM_AVAILABLE 64
windows TLS结构示意图如下图所示
2、获得索引
- 在windows中使用TlsAlloc函数获得一个线程局部存储块的索引
DWORD TlsAlloc();
- 如果这个函数调用失败,则返回值是TLS_OUT_OFF_INDEXES(oxffffffff);如果这个函数调用成功,则会得到一个索引。
3、通过索引:存储数据、取出数据
- 接下来就可以利用如下两个API函数分别在这个索引指向的内存块中存储数据,或者在这个索引指向的内存块中取出数据了
LPVOID TlsGetValue(DWORD dwTlsIndex)
BOOL TlsSetValue(DWORD dwTlsIndex, LPVOID lpTlsValue);
4、释放索引和内存块
当不再需要索引指向的内存块时,就可以使用如下函数来释放索引和内存块:
BOOL TlsFree(DWORD dwTlsIndex)
5、编译器:提供定义线程局部变量的方法
当然,在使用线程局部存储时除了可以使用API函数,还可以使用 Microsoft VC++ 编译器提供的如下方法定义一个线程局部变量:
__declspec(thread) int g_mydata =1
6、demo
我们可以看到,task1线程的改变g_mydata ,并没有对线程task2产生影响;
#include <iostream>
#include<Windows.h>
#include<thread>
using namespace std;
__declspec(thread) int g_mydata = 1;
void task1()
{
while(true)
{
++g_mydata;
Sleep(1000);
}
}
void task2()
{
for (int i = 0; i < 10; i++)
{
cout << "g_mydata=" << g_mydata << ",threadid=" << this_thread::get_id() << endl;//windows系统提供的获取线程id的系统函数是GetCurrentThreadId();
Sleep(1000);
}
}
int main()
{
thread t1(task1);
thread t2(task2);
t1.join();
t2.join();
return 0;
}
输出
四、linux系统
1、简介
linux上的NTPL库提供了一套函数接口来实现线程局部存储
#include <pthread.h>
int pthread_key_create(pthread_key_t* key, void (*destructor)(void*));
int pthread_key_delete(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void* value);
void* pthread_getspecific(pthread_key_t key);
pthread_key_delete:为线程局部存储创建一个新的key.
pthread_setspecific:设值
pthread_getspecific:获取值
参数destructor是一个自定义函数指针,其签名如下
void* destructor(void* value)
{
// 多是为了释放value指针指向的资源
}
线程终止时,如果key值管理的值不是NULL,那么NTPL会自动指向定义的destructor函数;如果无需解构他,则可以将destructor设置为NULL;
5、编译器:提供定义线程局部变量的方法
和Windows一样,linux的gcc也提供了一个关键字__thread用于定义线程局部变量;clang也是。
6、demo
略
五、C++11
1、简介:thread_local
虽然windows和Linux都有各自的方法声明线程局部存储变量,但是其使用范围和规则却存在一些区别,这种情况增加了c++的学习成本,也是c++标准委员会不愿意看到的。
于是,在c++11标准中整数添加了新的tread_local说明符来声明线程局部存储变量。
thread_local int g_mydata =1;
有了这个关键字,是哦那个线程局部存储的代码就可以同时在windows和 linux上运行了。
2、能与static或extern结合
thread_local说明符可以 用例声明线程生命周期的对象,它能与static或extern结合,分别指定内部或外部链接,不过额外的static并不影响对象的生命周期。换句话说,static并不影响线程局部存储的属性。
#include <iostream>
#include<Windows.h>
#include<thread>
using namespace std;
struct X {
thread_local static int i;
};
thread_local X a;
int main()
{
thread_local X b;
return 0;
}
3、demo
把window下的简单改下关键字,就是c++11的demo
#include <iostream>
#include<Windows.h>
#include<thread>
using namespace std;
thread_local int g_mydata = 1;
void task1()
{
while(true)
{
++g_mydata;
Sleep(1000);
}
}
void task2()
{
for (int i = 0; i < 10; i++)
{
cout << "g_mydata=" << g_mydata << ",threadid=" << this_thread::get_id() << endl;//windows系统提供的获取线程id的系统函数是GetCurrentThreadId();
Sleep(1000);
}
}
int main()
{
thread t1(task1);
thread t2(task2);
t1.join();
t2.join();
return 0;
}
输出
六、线程局部存储重点
- 1、对于线程变量,每个线程都会有该变量的一个拷贝,互不影响,该局部变量一直存在,直到线程退出;
- 2、系统的线程局部存储区域的内存空间并不大,所以尽量不要用这个空间存储大的数据块,如果不得不使用大的数据块,则可以将大的数据块存储在堆内存中,再将该堆内存的地址指针存储在线程局部存储区域。
参考:
1、《现代c++语言核心特性解析》谢丙堃 著;
2、《c++服务器开发精髓》 张远龙 著;