Win32多线程编程 — 线程局部存储

预留内存携带附加信息的设计

有时候,将数据与一个对象的实例关联起来是很有帮助的。这种设计要求预留一定的内存,一倍特定附加数据的存储。

通过调用SetWindowWordSetWindowLong函数将数据与一个指定的窗口关联起来,数据保存在窗口附加内存块中。窗口内存块即是一种窗口对象(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)线程都维持了一个消息队列。GetMessageTranslateMessageDispatchMessage等对消息的操作都是与调用线程的消息队列息息相关。PostThreadMessage是线程消息投递函数,它向一个指定ID(idThread)的线程发送一条消息,然后不等处理立即返回。这个API在多线程架构程序中非常有用。PostQuitMessage是结束线程运行,相当于nExitCode作为WM_QUIT消息参数调用PostThreadMessage。调用线程收到该消息后即ExitThread,故该函数一般用来响应WM_DESTROY消息。

尽管秉持封装的原则,我们极力强调避免使用全局变量,但全局变量对于进程级和线程级的系统统筹管理却是非常有用。除了消息队列这种系统内置的线程私有数据外,Windows提供了线程局部存储系统(TLS,Thread Local Storage),为用户提供了存储与线程关联数据的接口。前面提到的_beginthreadex中分配的_ptiddatapointer to per-thread data),即使用了TLS。_ptiddata为Windows平台的多线程程序中,strtokstrerrorerrno等依赖全局变量或静态变量的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函数为线程局部存储分配索引,函数原型如下。

DWORD TlsAlloc(VOID);

TlsAlloc为我们预订了一个索引。如果TlsAlloc返回的索引为3,那等于说索引3已经被我们预订了,无论是进程中当前正在运行的线程,还是今后可能会创建的线程,都不能再使用索引3。

(2)每个线程调用TlsSetValueTlsGetValue设置或读取线程数组中的值,这两个函数的原型如下。

BOOL TlsSetValue(

               DWORD dwTlsIndex,  // TLS index

               LPVOID lpTlsValue  // value to store

);

 

LPVOID TlsGetValue(

                 DWORD dwTlsIndex   // TLS index

);

(3)主线程调用TlsFree释放局部存储索引。函数的惟一参数是TlsAlloc返回的索引。

BOOL TlsFree(

            DWORD dwTlsIndex   // TLS index

            );

 

MFC中的线程局部存储

如果你需要大量的数据贯穿一个线程,普通的TLS索引一个值就会变得不实用,Windows的TLS只允许用户保存一个32位的指针。如果需要用户保存任意类型的数据(包含整个类)。这个任意大小的数据所占的内存通常是在进程的堆中分配,所以当用户释放全局索引时,系统必须将每个线程内此数据占用的内存释放掉,这就要求系统把为各线程分配的内存都记录下来。较好的方法是将各个私有数据的首地址用一个链表连在一起,释放全局索引时只要遍历此链表,就可以逐个释放线程私有数据占用的空间了。

例如,有下面一个存放线程私有数据的数据结构。

struct CThreadData

{

    CThreadDatapNext// 指向下一个线程的CThreadData结构的指针

    LPVOID pData;       // 指向真正的线程私有数据的指针

};

指针 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中的CHandleMapm_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对象的线程。通常,这将需要使用前面提到的同步机制,以保证多线程数据交换的一致性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
侯捷的<深入浅出MFC>相信大家都已经很熟悉了,论坛上也有很多介绍,这里我就不多说了。 而李久进的<MFC深入浅出>,听说的人可能就少得多。原因听说是这本书当时没有怎么宣传,而自从1999年第1版后,似乎也没有重印过,现在市面上根本找不到,所以大部分人都不知道。我手里现在恰好有一本,是从图书馆借的。这本书全名为<MFC深入浅出——从MFC设计到MFC编程>李久进编著,华中理工大学出版。此书极佳! 我这本书是1999年9月第一版,印数居然只有5000册。这么好的书只印5000册,而市面上都让一堆破烂玩意充斥着…… 这本书在写作目的上和侯捷的那本<深入浅出MFC>很相像。都是具体介绍MFC的原理和MFC的设计的。 看有的帖子说这本书难,这倒没有感觉到。当然,我看这本书的时候已经学完MFC的很多东西了,也看过侯捷那本。不过,这本书确实不大适合入门。而比较适合精通。 和侯捷那本书比起来,这两本书的风格很不一样。侯捷的那书的特点是剖了很多MFC的源代码,喜欢用代码说明问题,包括自己模拟MFC的方面实现一个类似的构架(什么什么仿真),而李久进的那本书不是这样,他用了很多的图表,具体介绍了MFC干很多事的时候的具体过程,比如MFC创建的时候及退出的时候具体的调用函数的过程(具体函数的调用关系)。这觉得这部分极为重要,这也就是我推崇李久进这本书的原因。而侯捷的那本书这部分内容非常少。这想,这可能是由于侯捷觉得,这部分内容不需要单独介绍,大家自己剖代码就可以解决问题。这确实不错,李久进的那本书中的内容如果自己剖MFC的源代码,内容都可以找到。但这是一个非常花工夫的事情,更不要说MFC的实际代码中要考虑各种各样的问题(保护,检查),代码的思路不可能非常清楚,这无疑增加了读代码的难度。和自己花时间一点一点剖MFC代码相比,看看这本只有266页的书无疑有效得多。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值