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

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

通过调用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函数为线程局部存储分配索引,函数原型如下。

DWORD TlsAlloc(VOID);

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

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

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

{

CThreadData* pNext; // 指向下一个线程的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中的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下线程和窗口的数据绑定》

Win32多线程编程(5) — 线程局部存储相关推荐

  1. Win32多线程编程(2) — 线程控制

    Win32线程控制只有是围绕线程这一内核对象的创建.挂起.恢复.终结以及通信等操作,这些操作都依赖于Win32操作系统提供的一组API和具体编译器的C运行时库函数.本篇围绕这些操作接口介绍在Windo ...

  2. Win32多线程编程(3) — 线程同步与通信

    一.线程间数据通信 系统从进程的地址空间中分配内存给线程栈使用.新线程与创建它的线程在相同的进程上下文中运行.因此,新线程可以访问进程内核对象的所有句柄.进程中的所有内存以及同一个进程中其他所有线程的 ...

  3. 多线程编程之三——线程间通讯

    七.线程间通讯 一般而言,应用程序中的一个次要线程总是为主线程执行特定的任务,这样,主线程和次要线程间必定有一个信息传递的渠道,也就是主线程和次要线程间要进行通信.这种线程间的通信不但是难以避免的,而 ...

  4. C#多线程编程实例 线程与窗体交互

    C#多线程编程实例 线程与窗体交互 代码: public partial class Form1 : Form{//声明线程数组Thread[] workThreads = new Thread[10 ...

  5. 多线程编程(2): 线程的创建、启动、挂起和退出

    python多线程编程(2): 线程的创建.启动.挂起和退出 如上一节,python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法.而创建 ...

  6. 多线程编程:线程死锁的原因以及解决方法

    多线程编程:线程死锁的原因以及解决方法 关于线程死锁这个问题相信程序员在编写多线程程序时会经常遇到的一个经典问题,这种情况往往出现在多个线程同时对临界资源访问时所产生的. 属于临界资源的硬件有打印机. ...

  7. Python多线程编程之线程子类化

    Python多线程编程之线程子类化 基本思路 Threading模块简介 **MyThread**主要代码 实例 所有代码 运行结果 基本思路 导入Threading模块下的Thread类,将其子类化 ...

  8. Win32多线程编程(1) — 基础概念篇

    内核对象的基本概念 Windows系统是非开源的,它提供给我们的接口是用户模式的,即User-Mode API.当我们调用某个API时,需要从用户模式切换到内核模式的I/O System Servic ...

  9. C++多线程编程分析-线程间通信

    上文我们介绍了如何建立一个简单的多线程程序,多线程之间不可避免的需要进行通信.相比于进程间通信来说,线程间通信无疑是相对比较简单的. 首先我们来看看最简单的方法,那就是使用全局变量(静态变量也可以)来 ...

最新文章

  1. VRRP在企业网中的应用(H3C设备)
  2. JAVA对象转为Java String的几种常用方法
  3. java的虚引用_你不可不知的Java引用类型之——虚引用
  4. 最短路径算法 Floyd-傻子也能看懂的弗洛伊德算法(转)
  5. Singleton设计模式
  6. 【Linux】一步一步学Linux——read命令(220)
  7. 数据库连接池和connection的理解
  8. python 内置方法赋值_Python内置数据结构之字符串str
  9. python 把xml中含有特殊字段的部分提取出来_Python: 爬虫网页解析工具lxml.html(一)...
  10. ci框架中引入css,php ci框架中载入css和js文件失败的原因及解决方法
  11. 计算机键盘输入法基础知识,教程计算机基础知识-:认识输入法
  12. Oracle 单实例 迁移到 RAC 实例 -- 使用RMAN 异机恢复
  13. 【Unity】Geometry Shader实现
  14. html无限弹窗关不掉,使用cmd命令时出现无限弹窗的故障如何终止
  15. 启明星辰天玥网络安全审计系统手册
  16. windows常用指令大全
  17. 如何利用阿里物联网做一个会自动浇水的花盆
  18. div和div之间画横线_HTML在两个div标签中间画一条竖线的方法
  19. 迁移学习domain adaption
  20. 【防坑指南】nginx重启后出现[error] open() “/usr/local/var/run/nginx/nginx.pid” failed

热门文章

  1. java调用存储过程之环境测试
  2. 网页加载的不同的方式、点击按钮显示一句话、jQuery中获取元素的方法
  3. 测试原理_OTDR测试原理及注意事项
  4. java进制原码_Java 一一 进制、原码 反码 补码、移位操作
  5. Oracle 创建主键自增表
  6. 关于使用安装Adobe绿色精简版所需运行库
  7. 转 Python爬虫入门二之爬虫基础了解
  8. Debian 里设置 IP 地址、网关、DNS
  9. 用Linux命令行生成随机密码的十种方法
  10. [JavaScript]只需一行代码,轻松搞定快捷留言功能