线程切换
一般的PC都是只有一个CPU,而多个线程的运行使得电脑看起来好像同时在做很多事,此是通过一会做这件事,一会再做另一件事来实现的。即操作系统会频繁地让多个线程各执行一小段时间(被称为时间片),以期望通过足够的频繁而使得其好象在同时执行多个线程。当某个执行中线程执行够了它的时间片后,操作系统就会通过某些操作让另一个线程进入运行,而前者则停止运行,这里的“某些操作”就是所谓的线程切换。其具体操作就是将当前线程的运行环境(Context)保存起来,如将CPU各寄存器的值存到一个TLS(后叙)内存中,然后将欲运行的线程的运行环境,加载到当前CPU硬件以让线程运行,因此线程切换就是线程运行环境的保存和加载。
在《
COM线程模型》中提到线程切换是非常耗时的,而操作系统却又极其频繁地发生线程切换,那么操作系统岂不是非常地效率低下?不是。《
COM线程模型》中所谓的线程切换的耗时并不是线程切换本身耗时,而是线程发生运行方式转换(从用户方式转为内核方式),此操作一般会有1000个以上的CPU周期,此即所谓的耗时。
Windows中的线程可以运行在两种方式下,内核方式(Kernel Mode)和用户方式(User Mode)。
内核方式下是可以直接访问物理内存的,而不是进程的虚拟内存空间,此时具有多种特权。此时如果发生错误,可能是整个操作系统挂起(Halt)而不是简单地用任务管理器(或类似软件)就可以搞定的。此一般是内核代码或硬件驱动程序的工作方式。
用户方式则是普通的运行方式,只能通过进程的虚拟内存空间的映射来访问物理内存,没有特权。
Windows有三种对象:用户对象(User Object)、GDI对象(GDI Object)和内核对象(Kernel Object)。内核对象所使用的内存是在内核模式下才可访问的内存。Windows对每种内核对象都提供了一系列的API以对其操作。当调用这类API时,线程必须从用户方式转换成内核方式以操作内核对象所在的内存,这也就导致了前面提过的损耗。
当我们进行线程同步时,会调用类似WaitForSingleObject之类的等待函数,此时线程会挂起,等待指定的内核对象处于通知状态,此时就会发生上面的损耗。而COM缺省提供的线程同步功能由于其灵活性注定了不能使用用户方式的同步(如原子访问、关键代码段),因此在STA线程和MTA线程之间及各自之间的同步就使用了内核同步对象(如事件、互斥量等)来进行同步,也就导致了上面提到的损耗,也就是NA套间产生的原因及目的。
线程局部存储(Thread Local Storage)
线程局部存储(TLS)是Windows提供的一种技术,用于将一些内存和一线程关联起来,这样即使同样的代码,不同的线程访问,实际将会访问不同的内存,这和线程的堆栈是一样的道理。
那为什么不直接使用堆栈还要来个TLS?因为堆栈相当于是一个历史记录,里面的内存数值与调用顺序有着密切关系。如果希望多个函数间共享一块内存,应该使用全局变量,但是由于又想线程相关,决定将内存分配在栈上(不过这不重要),则在执行函数时必须准确知道内存分配在栈上的什么位置(也就是地址),而这个位置又需要通过另一个全局变量来进行函数间传递,因此又需要栈上一个内存,又……。这是一个死循环,这也就是为什么有TLS的存在。
TLS共提供了4个API,分别为TlsAlloc、TlsSetValue、TlsGetValue和TlsFree。调用TlsAlloc将得到一个cookie,是一个DWORD值,是个序号。然后分配一块内存,将内存的地址通过TlsSetValue和前面得到的cookie保存起来,然后在适当的时候调用TlsGetValue得到记录的地址,程序不再使用TLS的时候调用TlsFree释放前面的cookie即可。
上面的关键就在于不同的线程调用TlsGetValue,即使提供同样的cookie,返回的也不是同一个值。同样,不同的线程调用TlsSetValue,即使同样的cookie,却不是互相干涉,并且在TlsGetValude时能正确返回。故上面的cookie可以是个全局变量。如:
#include <stdio.h> #include <windows.h>
// 出于样例,不做任何错误检查及处理 DWORD g_Cookie = static_cast< DWORD >( -1 ); DWORD g_Index = 1; struct ABCD { long a; }; void CBA() { reinterpret_cast< ABCD* >( TlsGetValue( g_Cookie ) )->a = g_Index++; Sleep( rand() % 1000 ); } DWORD WINAPI AB( LPVOID ) { ABCD *pTemp = new ABCD; TlsSetValue( g_Cookie, pTemp ); CBA(); printf( "%d\n", pTemp->a ); delete pTemp; return 0; } void main() { g_Cookie = TlsAlloc();
DWORD id = static_cast< DWORD >( -1 ); HANDLE hThreads[4]; for( unsigned long i = 0; i < 4; ++i ) hThreads[ i ] = CreateThread( NULL, 0, AB, NULL, 0, &id );
WaitForMultipleObjects( 4, hThreads, TRUE, INFINITE );
for( i = 0; i < 4; ++i ) CloseHandle( hThreads[ i ] );
TlsFree( g_Cookie ); } |
输出结果如下(注意其中并没有因为g_Index++而是顺序的):
1
3
2
4