赞
踩
六、线程基础
进程有两个组成部分:一个进程内核对象和一个地址空间。类似地,线程也有两个组成部分:
1)一个是线程的内核对象,操作系统用它管理线程。系统还用内核对象来存放线程统计信息的地方。
2)一个线程栈,用于维护线程执行时所需的所有函数参数和局部变量。
一个进程中的所有线程共享同一个地址空间,共享同一个句柄表。
创建和初始化一个线程
CreateThread函数使系统创建一个线程内核对象,最初的使用计数为2,其它属性也被初始化:暂停计数设为1,退出代码为STILL_ACTIVE(0x103),对象为未触发状态。
系统分配内存供线程的堆栈使用,然后系统将两个值写入线程堆栈的最上端(线程堆栈由高位到低位构建),第一个值是传给CreateThread的线程传入参数,第二个值是函数入口指针。
每个线程有自己的一组CPU寄存器,称为线程的上下文(context)。线程CPU寄存器全部保存在一个CONTEXT结构里,该结构放在线程内核对象中。如上图所示。
线程的内核对象被初始化的时候,CONTEXT结构的堆栈指针寄存器(SP)设为线程函数指针(pfnStartAddr)在线程堆栈中的地址,而指令指针寄存器(IP)转为一个称为BaseThreadStart的函数地址中。这个函数的基本操作是:
VOID BaseThreadStart( PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam ) {
__try{
ExitThread((pfnStartAddr)(pvParam);
}
__except( UnhandledExceptionFilter(GetExceptionInformation())){
ExitProcess(GetExceptionCode());
}
}
这个函数实际就是线程开始执行的地方。它并没有由别的函数调用,之所以能够访问这两个参数,是因为这两个参数的值显式地写入了线程堆栈中(如同普通的函数的参数传入方式一样)。
这个函数会调用ExitThread或ExitProcess,所以线程永远不能退出函数,始终在内部“消亡”,因此BaseThreadStart是一个VOID返回类型。
多线程环境中会有一些C/C++运行库变量和函数,比如errno, strtok等,为了保证C/C++多线程应用程序正常运行,必须创建一个数据结构,并使之与使用了C/C++运行库函数的每个线程关联,这样在调用C/C++运行库函数的时候,那些函数必须查找主调函数的数据块,从而避免影响到其它线程。因此创建线程时,应该使用C/C++运行库函数_beginThreadex,它会给线程创建专用的_tiddata内存块,使用线程局部存储与主调线程关联起来。线程函数正常执行完成之后,会调用_endthreadex来将些数据块释放。
如果用CreateThread创建线程,当线程调用一个需要_tiddata结构的C/C++运行库函数时,该函数尝试取得线程数据块,若没有,就会分配并初始化一个_tiddata块,并与线程关联,这样带来的问题是,如果线程不是通过调用_endthreadex来终止,数据块就不会被销毁,从而导致内存泄漏(使用CreateThread函数创建的线程,自然不会调用_endthreadex)。
函数:HANDLE GetCurrentProcess()和 HANDLE GetCurrentThread()可以返回得到主调线程的进程内核对象和线程内核对象的一个伪句柄,它们不会在句柄表中创建新的句柄,也不会影响进程或线程内核对象的使用计数,使用CloseHandle来关闭伪句柄时,会返回FALSE,使用GetLastError将返回ERROR_INVALID_HANDLE。
可以使用GetThreadTimes查询自己的线程时间,如果一个父线程向子线程传递一个可以自己的伪句柄给子线程,子线程将它作为参数调用GetThreadTimes将会得到子线程的线程时间,因为传入的句柄是个伪句柄,而不是一个真正的句柄,线程的伪句柄是一个指向当前线程的句柄(发出函数调用的那个线程)。
要将一个伪句柄转换成一个真正的句柄,使用DuplicateHandle,这个函数会递增内核对象的使用计数,所以在使用完复制的对象句柄之后,应该CloseHandle。
七、线程调试、优先级和关联性
如果一个线程被挂起三次,那么在它有资格让系统给它分配CPU之前必须恢复三次。线程的挂起计数大于0,就意味着该线程已经被挂起。SuspendThread会挂起一个线程,并返回之前的挂起计数。ResumeThread则恢复一个线程,也返回之前的一个挂起计数。
如果线程告诉系统,在一段时间内自己不需要调试了,可以通过调用Sleep实现。
系统提供了一个名为SwitchToThread的函数,如果存在另一个可调试的线程,系统会让此线程运行。调用SwitchToThread和Sleep(0)类似,区别在于,SwitchToThread允许执行低优先级线程,Sleep会立即重新调度主调线程,即使低优先级线程还处于饥饿状态。
获取进程优先级:DWORD GetPriorityClass( HANDLE hProcess );
改变进程的优先级:BOOL SetPriorityClass( HANDLE hProcess, DWORD fdwPriority );
对应的对线程的优先级操作有SetThreadPriority和GetThreadPriority
Windows会动态提升线程的优先级(只提升值在1~15的线程,不能动态提升实时范围(16~31)的线程),如果不希望系统进行动态提升,可以使用SetProcessPriorityBoost和SetThreadPriorityBoost,相应的获取函数为GetProcessPriorityBoost和GetThreadPriorityBoost。
如果要限制某些线程只在可用CPU的一个子集上运行,可以调用 BOOL SetProcessAffinityMask( HANDLE hProcess, DWORD_PTR dwProcessorAffinityMask ),第二个参数是一个位掩码,代表线程可以在哪些CPU上运行。对应的可以返回进程的关联性掩码,GetProcessAffinityMask。
八、用户模式下的线程同步
所有线程都应该调用Interlocked系统函数来修改共享变量的值,而不应该使用简单的C++语句来修改共享变量。
InterlockedExchangeAdd, InterlockedExchangeAdd64, InterlockedExchange, InterlockedExchange64, InterlockedExchangePointer, InterlockedCompareExchange, InterlockedCompareExchangePointer, InterlockedIncrement, InterlockedDecrement。
关键段(critical section)是一小段代码,在它执行之前需要独占对一些共享资源的访问权。代码知道除了当前线程之外,没有其他线程会同时访问该资源(在当前线程离开关键段之前)。在代码中先定义一个CRITICAL_SECTION数据结构,然后把任何需要访问共享资源的代码放在EnterCriticalSection和LeaveCriticalSection之间。关键段最大的缺点在于它们无法用来在多个进程之间对线程进行同步。
EnterCriticalSection访问被占用的资源的时候,线程会被切换到等待状态,如果不希望如此,可以使用TryEnterCriticalSection,如果它发现资源正在被其他线程访问,那么它会返回FALSE,否则返回TRUE。如果返回值为TRUE,对应的需要调用一个LeaveCriticalSection。
当线程试图进入一个关键段,但这个关键段正在被另一个线程占用的时候,函数会立即把调用线程切换到等待状态,这意味着线程必须从用户模式切换到内核模式(大约1000个CPU周期),这个切换开销非常大。尤其是当需要等待的线程完全切换到内核模式之前,占用资源的线程可能就已经释放了资源,这种情况会浪费大量的CPU时间,为此,可以使用旋转锁,在初始化的时候使用下面的函数:
BOOL InitializeCriticalSectionAndSpinCount(
PCRTICAL_SECTION pcs,
DWORD dwSpinCount );
这样,当EnterCriticalSection的时候,它会用一个旋转锁不断地循环,尝试在一段时间内获得对资源的访问权,只有当尝试失败的时候,线程才会切换到内核模式并进入等待状态。确定dwSpinCount的值通常需要尝试才能得到最佳性能。用来保护进程堆的关键段所使用的旋转次数大约是4000。
Vista之后,WINDOWS提供了slim读/写锁,SRWLock的目的和关键段相同,但它区分那些想要读取资源值的线程(读取者线程)和想要更新资源的值的线程(写入者线程)。所有读取者线程在同一时候读取资源是可行的,因为读取不会破坏数据。只有当写入者线程想要对资源更新的时候才需要进行同步,因此,写入者线程会独占对资源的访问权:任何其他线程,无论是读取者线程还是写入者线程,都不允许访问资源。
首先分配一个SRWLOCK结构并用InitializeSRWLock来初始化,写入者线程可以调用AcquireSRWLockExclusive来尝试获得对保护资源的独占访问权。完成对资源的更新之后,就调用ReleaseSRWLockExclusive解除对资源的锁定。
对读取者线程来说,调用的函数则是:AcquireSRWLockShared和ReleaseSRWLockShared。
有时我们想让线程以原子方式把锁释放并将自己阻塞,直到某一个条件成立为止,WINDOWS提供了一种条件变量,通过SleepConditionVariableCS或SleepConditionVariableSRW函数。当另一个线程检测到相应的条件已经满足的时候,调用WakeConditionVarable或WakeAllConditionVariable来唤醒Sleep中的线程。
九、用内核对象进程线程同步
等待函数使一个线程自愿进入等待状态,直到指定的内核对象被触发为止。(如果已经被触发,则不会进入等待状态),最常用的是WaitForSingleObject。WaitForMultipleObjects则允许调用线程同时检查多个内核对象的触发状态。
等待成功的副作业:对一些内核对象来说,成功调用等待函数事实上会改变对象的状态。比如一个自动重置事件对象,当事件被触发,等待函数返回,但在返回之前,它会使事件变为非触发状态——这就是等待成功所引起的副作用。
内核对象与线程同步
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。