[转]TLS: 线程局部存储TLS
线程局部存储TLS
堆栈中定义的局部变量,对多线程是安全的,因为不同的线程有自己的堆栈。而通常定义的全局变量,所有线程都可以作读写访问,这样它就不是线程安全
的,为安全就有必要加锁互斥访问。而何为线程局部存储(TLS),简单的说,就是一个变量(通常是指针,指向具体的类型),每个线程都有一个副本,而在程
序中可以按照相同的方式来访问,(比如使用相同的变量名,又或者都调用TlsGetValue),既然是都有副本,自然线程中互不影响。打个比方,就如同
一个人,被克隆出三个,其中一个被砍了一刀,其它两人都不会受伤。
VC编译器可以隐式的定义线程局部变量,只要定义的时候加上__declspec (thread)前缀。比如
__declspec (thread) int iGlobal_1 = 1;
__declspec (thread) double iGlobal_2 = 2.0;
iGlobal_1,iGlobal_2就都有自己的副本。
另外windows也提供了几个api, 来显式定义线程局部变量。这几个api为TlsAlloc, TlsFree, TlsSetValue, TlsGetValue。用法自己查查。
好,现在可以入正题。来说说它的实现。
应该都知道,操作系统会使用一个结构来描述线程,这结构通常称为
TEB((Thread Environment Block) ,
每个线程有一个对应的TEB,切换线程的时候,也会切换到不同的TEB。有某个指针值指向当前的TEB,
切换线程的时候就改变这个指针值,这样访问线程相关的数值,就可以统一从这个指针值找起。windows中,这个线程指针值放在fs寄存器。
TEB
里面有些什么变量呢?当然是跟线程有关的变量啦。更具体的自己去查资料。其中有个变量是线程TLS数组的指针。称为_tls_array,利用这个数组就
可以管理线程相关的数据了。_tls_array_指针在windows中处于TEB偏移0x2h的地方。结合上面说的,看到mov ecx,
dowrd ptr fs:[2ch],就应该知道是取当前线程的_tls_array的指针,放在ecx寄存器中。
现在,我们在不同的线程中已经可以取得各自的_tls_array,这时候,要访问数组的元素,还差索引。这时,再看看TlsAlloc,
你应该很清楚它的意思?无错,它就是说,请为我分配一个索引号,表示相应的数组项已被使用。TlsFree,
就是释放索引号,表示相应的数组项可以被再次使用。TlsSetValue,TlsGetValue就是拿个索引,向相应的数组项设值或者取值。
好好想想,为什么我有个相同索引号,在不同的线程中调用TlsGetValue,取出来的值会不同呢?因为数组的起始指针已经变了。在计算
机中反反复复都会出现
基址+偏移的模式。基址不变,偏移变,取的值不同;基址变,偏移不变,取的值也不同。这看起来很简单,但很可能没有意识到这点。比如为什么C++中,不同
的对象,都有变量m_a,
它可以有不同值呢,因为不同的对象基址也不同(也就是this指针),而为什么它们可以用相同的代码来访问变量m_a,因为变量m_a的相对偏移是不变
的。
回到TLS,
索引号对应的是偏移,因为线程切换引起了_tls_array数组的切换,
因此取值可以不同。这样用TlsAlloc分配了一个索引号,所有线程中_tls_array的对应元素其实都已经是归你所有,并非只是当前线程。再次强
调,分配出一个索引,所有线程的_tls_array数组中的索引对应项都已经被分配,有5个线程,你可以管理的已经有5个格子,并非只是当前线程的一个
格子。又因为索引是一样的,就可以用相同的方式来使用这些数组的小格子。现在你已经有个小格子了,可以往里面放东西了,放什么东西你可以自己确定,你可以
放指针,或者放整数,或者放字符。因为是自己放的,自己可以知道意思,取出来对你就是有用的。这里,又引起另一个很简单但又很容易忽略的问题,内存中放的
究竟是什么?数字?可以说是,但更准确的是放状态,只不过这状态可以用数字来编码(任何东西都可以用数字来编码,只要你懂得解码的方式,这串数字对你就是
有意义的)。 2bit, 可以表示4个状态,4bit可以表示16个状态,32bit可以表示4G个状态。
上面其实已经说完了显式TLS分配,也就是调用TlsAlloc等方式。那隐式的TLS分配,又是怎么实现的呢?
第一次调用TlsAlloc, 检查返回值,会发现返回1,为什么是1,而不是0呢? 因为0已经被使用了,谁在使用? 编译器。
定义
__declspec (thread) int iGlobal_1 = 1;
__declspec (thread) double iGlobal_2 = 2.0;
时
候,生成了一个段.tls,
这个段中有这两个数据,保持下来放在执行文件中。当程序运行,每个线程会将.tls复制一份。线程_tls_array的0索引号被占据,对应的格子就放
着指向这份数据的指针(即指向已分配的数据)。比如上面的语句,你可以想象成隐式定义了一个结构
struct TLS_Data
{
int iGlobal_1;
double iGlobal_2;
};
每
个线程运行时,new出这个结构,在用TlsSetValue就指针设置到线程各自的_tls_array,0索引对应的位置。因为不同变量的偏移值在结
构中是不同的,这样基址变,偏移不变,取得线程各自的TLS_Data结构,再跟着基址不变,偏移变,就可以访问不同的变量。
分析一下
__declspec (thread) int iGlobal = 1;
int main()
{
int i = iGlobal;
return 0;
}
的汇编代码
mov eax,[__tls_index] // 将索引放在eax中,通常为0
mov ecx,dword ptr fs:[2Ch] // 将线程对应的_tls_array指针放在ecx中
mov edx,dword ptr [ecx+eax*4]
// 每个格有4 byte, 取出_tls_array数组元素,放在edx中,这数组元素放着的是我们假象的TLS_Data结构指针
mov eax,dword ptr [edx+104h] // 指针加上变量在结构中的偏移,取得iGlobal变量值
mov dword ptr [ebp-4],eax // 将iGlobal变量值放在栈变量,也就是i中
注意,iGlobal的偏移并不是0, 因为已经有些变量定义了,比如线程各自的errno变量,strtok函数用的变量等等。
可以这样说,编译器拿出_tls_array的一个格子,自
己管理,又再实现出另一种风格的TLS。我们也可以自己用TlsAlloc取得一个索引,跟着 new
出另一个子数组,子数组指针放在_tls_array元素中。真正的数据指针放在子数组中。这样,我们就可以根据自己的需要来实现自己的线程局部存储。而
又不占用多个_tls_array数组的索引。
再来讨论一下_tls_array数组的索引的分配跟释放。一定要有某种方式来标记着那个索引被分配了,那个索引还可以使用。如果
_tls_array一定要放指针,那我们可以将没有分配的索引元素设置为NULL,
已分配的非NULL,从前到后检查数值,取第一个NULL元素索引分配出去。但因为数组不一定放指针,也可以放整数,整数没有所谓的无效值,就不能用这种
方式。你可以创建一个同样大小的bool数组作标记,也可以采用位判断方式来替代bool数组。如果有需要,还可以定义自己的结构作标记用。但一定要有种
方式来区别分配的索引号跟没有分配的索引号。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/complex_ok/archive/2009/07/15/4351673.aspx