DoubleLi

qq: 517712484 wx: ldbgliet

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

 

 

简介:
对于一个存在多个线程的进程来说,有时需要每个线程都自己操作自己的这份数据。这有点类似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++服务器开发精髓》 张远龙 著;

 
posted on 2023-03-16 16:56  DoubleLi  阅读(658)  评论(0编辑  收藏  举报