textbox

IT博客 联系 聚合 管理
  103 Posts :: 7 Stories :: 22 Comments :: 0 Trackbacks
TLS

转载  [转]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

posted on 2010-09-27 12:41 零度 阅读(637) 评论(0)  编辑 收藏 引用 所属分类: 概念
只有注册用户登录后才能发表评论。