Win32多线程编程(5) ― 线程局部存储


Win32多线程编程(5) ― 线程局部存储
2010年08月11日
  预留内存携带附加信息的设计
  有时候,将数据与一个对象的实例关联起来是很有帮助的。这种设计要求预留一定的内存,一倍特定附加数据的存储。
  通过调用SetWindowWord或SetWindowLong函数将数据与一个指定的窗口关联起来,数据保存在窗口附加内存块中。窗口内存块即是一种窗口对象(HWND)的附加数据(window extra bytes),参考WNDCLASS.cbWndExtra字段(Specifies the number of extra bytes to allocate following the window instance.)。
  这种预留附加的设计,在MFC中处处可见。对于下拉选择列表(CComboBox)、下拉列表框、列表视图和树控件,我们不光希望其能显示条目内容(item text),还希望每个条目能够携带附加信息,即存储额外的关联数据(item data),以备不时之需。这四个控件都提供了SetItemData/GetItemData接口,供用户储存关联数据。存储的数据为DWORD值类型,可以是简单的数值,也可以存储指针。
  线程消息队列和_ptiddata
  我们在编写第一个SDK窗口程序时,就接触到了消息这一重要概念。实际上,消息队列是一种线程私有数据,每一个Windows程序的UI(CUI/GUI)线程都维持了一个消息队列。GetMessage、TranslateMessage、DispatchMessage等对消息的操作都是与调用线程的消息队列息息相关。PostThreadMessage是线程消息投递函数,它向一个指定ID(idThread)的线程发送一条消息,然后不等处理立即返回。这个API在多线程架构程序中非常有用。PostQuitMessage是结束线程运行,相当于nExitCode作为WM_QUIT消息参数调用PostThreadMessage。调用线程收到该消息后即ExitThread,故该函数一般用来响应WM_DESTROY消息。
  尽管秉持封装的原则,我们极力强调避免使用全局变量,但全局变量对于进程级和线程级的系统统筹管理却是非常有用。除了消息队列这种系统内置的线程私有数据外,Windows提供了线程局部存储系统(TLS,Thread Local Storage),为用户提供了存储与线程关联数据的接口。前面提到的_beginthreadex中分配的_ptiddata(pointer to per-thread data),即使用了TLS。_ptiddata为Windows平台的多线程程序中,strtok、strerror(errno)等依赖全局变量或静态变量的CRT函数的实现提供了有效的解决方案。
  Win32线程局部存储系统
  用于管理 TLS 的数据结构是很简单的,Windows仅为系统中的每一个进程维护一个位数组,再为该进程中的每一个线程申请一个同样长度的数组空间,如下图所示。
  
  在Windbg中,可以窥探TEB中的TLS数据结构。
  lkd> dt _teb
  nt!_TEB
  +0x02c ThreadLocalStoragePointer : Ptr32 Void
  +0xe10 TlsSlots         : [64] Ptr32 Void
  +0xf10 TlsLinks         : _LIST_ENTRY 
  +0xf94 TlsExpansionSlots : Ptr32 Ptr32 Void
  typedef struct _TEB// 66 elements, 0xFB8 bytes (sizeof)
  {
  // ……
  /*0x02C*/     VOID*        ThreadLocalStoragePointer;
  // ……
  /*0xE10*/     VOID*        TlsSlots[64];
  /*0xF10*/     struct _LIST_ENTRY TlsLinks; // 2 elements, 0x8 bytes (sizeof)
  // ……
  /*0xF94*/     VOID**       TlsExpansionSlots;
  // ……
  }TEB, *PTEB;
  当一个线程被创建时,Windows就会在进程地址空间中为该线程分配一个长度为TLS_MINIMUM_AVAILABLE的数组,数组成员的值都被初始化为 0。在内部,系统将此数组与该线程关联起来,保证只能在该线程中访问此数组中的数据。如上图所示,每个线程都有它自己的数组,数组成员可以存储任何数据。
  运行在系统中的每一个进程都有上图所示的一个位数组。位数组的成员是一个标志,每个标志的值被设为FREE或INUSE,指示了此标志对应的数组索引是否在使用中。Windows 保证至少有TLS_MINIMUM_AVAILABLE(定义在WinNT.h文件中)个标志位可用。
  动态使用TLS典型步骤如下。
  (1)主线程调用TlsAlloc函数为线程局部存储分配索引,函数原型如下。
  DWORDTlsAlloc(VOID);
  TlsAlloc为我们预订了一个索引。如果TlsAlloc返回的索引为3,那等于说索引3已经被我们预订了,无论是进程中当前正在运行的线程,还是今后可能会创建的线程,都不能再使用索引3。
  (2)每个线程调用TlsSetValue和TlsGetValue设置或读取线程数组中的值,这两个函数的原型如下。
  BOOLTlsSetValue(
  DWORDdwTlsIndex,  // TLS index
  LPVOIDlpTlsValue  // value to store
  );
  LPVOIDTlsGetValue(
  DWORDdwTlsIndex   // TLS index
  );
  (3)主线程调用TlsFree释放局部存储索引。函数的惟一参数是TlsAlloc返回的索引。
  BOOLTlsFree(
  DWORDdwTlsIndex   // TLS index
  );
  MFC中的线程局部存储
  如果你需要大量的数据贯穿一个线程,普通的TLS索引一个值就会变得不实用,Windows的TLS只允许用户保存一个32位的指针。如果需要用户保存任意类型的数据(包含整个类)。这个任意大小的数据所占的内存通常是在进程的堆中分配,所以当用户释放全局索引时,系统必须将每个线程内此数据占用的内存释放掉,这就要求系统把为各线程分配的内存都记录下来。较好的方法是将各个私有数据的首地址用一个链表连在一起,释放全局索引时只要遍历此链表,就可以逐个释放线程私有数据占用的空间了。
  例如,有下面一个存放线程私有数据的数据结构。
  struct CThreadData
  {
  CThreadData* pNext; // 指向下一个线程的CThreadData结构的指针
  LPVOIDpData;       // 指向真正的线程私有数据的指针
  };
  指针 pData指向为线程分配的内存的首地址,指针pNext将各线程的数据连在了一起。这实际上是一种二级指针的分槽存储。MFC的线程局部存储类CThreadLocal即实现了二级指针的分槽存储。
  MFC框架的状态信息也是理解的难点,包括模块状态AFX_MODULE_STATE、线程状态_AFX_THREAD_STATE和模块线程状态AFX_MODULE_THREAD_STATE。这些线程级别的全局状态维持即使用了线程局部存储(TLS)。参考李久进著作的《MFC深入浅出》第九章《MFC的状态》。 由于MFC广泛地应用了线程局部存储,故在MFC下,使用线程必须格外小心。许多MFC对象仅在创建它们的线程内运作。一般地,具有句柄映射的任何对象都不能从其他线程访问该对象。例如,模块线程状态AFX_MODULE_THREAD_STATE中的CHandleMap* m_pmapHWND映射记录了MFC线程中创建的CWnd对象实例与内核窗口句柄(HWND)之间的映射消息。内核窗口句柄是可以进程访问级别,因此可跨线程访问。但是试图传递CWnd对象实例以期跨线程操作,往往失败。因为另一个引用线程并未像创建线程那样维系一个映射,所以当需要CWndàHWND以执行API操作时,往往找不到其所指窗口。
  针对以上问题,通常优先传送句柄,避免在线程之间传送MFC对象。在引用线程中将其转换为临时MFC对象。例如,假设线程 A创建一个CWnd对象。线程A并不将对象传送给线程B,而将该对象的m_hWnd成员传送给线程B。于是,线程B可以调用CWnd::FromHandle,以创建一个临时的CWnd对象。如果线程B需要更持久的连接,就可以使用Attach方法,在窗口及其CWnd对象之间建立持久的关联。
  另外的一个常见问题是MFC对象访存的线程安全性问题。MFC对象不会自动在不同的线程之间做出判断。所以,如果两个线程试图同时访问同一个CString类的对象,结果可能受到严重破坏。只有防止来自有冲突的MFC对象的线程。通常,这将需要使用前面提到的同步机制,以保证多线程数据交换的一致性。
  参考:
  《为什么要用TLS》
  《WIN32下线程和窗口的数据绑定》

猜你喜欢

转载自mbgodi.iteye.com/blog/1360867