线程是进程中可以被系统(作业系统)调度的最小单位。线程可以使用进程中的任何资源,他自身只包含有必要的独立的线程栈和寄存器。
线程的设计是为了提高CPU的使用率:当系统同时拥有多个CPU时让每个CPU都执行一个指令流在同一时间进行两个工作。当进程只拥有一个CPU的执行时间时,若执行流中发生阻塞(等待)而阻塞时不需要CPU执行任何指令,这时候可以让CPU去执行其他指令称之为线程切换,在阻塞解除又具备了指令继续执行下去的条件时CPU继续执行之后的指令看起来像是两个指令流同时在执行。这两种情况都可以称为并发。而CPU执行的线程属于同一个进程时称为多线程,属于不同进程时称为多进程,这两种都是并发的实现方式。考虑使用多线程或多进程时考虑他们的代价,多线程切换的代价远小于多进程切换的代价。对于有阻塞的情况(IO密集型)多采用多线程的方式,而对于线程能够一直占有进程的CPU(计算密集型)的情况一般采用多进程的方式。
Windows提供创建线程的API:
HANDLE CreateThread(
PSECURITY_ATTRIBUTE psa, //内核对象的安全属性
DWORD chStackSize, // 指定线程栈的大小
PTHREAD_START_ROUTINE pfnStartAddr, //指定线程函数的地址
PVOID pvParam, //传给线程函数的参数
DWORD dwCreateFlag, //指定线程的状态,创建后激活或者挂起,若为CREATE_SUSPEND需要调用ResumeThread运行
PDWORD pdwThreadID //返回值,记录线程ID,可以为NULL
);
与终止进程一样,线程可以通过以下四种方式终止运行:
1:线程函数返回。
2:线程调用ExitThread杀死自己。
3:其他线程调用TerminateThread。
4:进程终止导致线程终止。
与结束进程的方式一样,建议使用线程函数返回的方式来结束线程,其他方式结束线程将不会执行以下动作:
当结束时线程能够保证下列动作会被执行(确保系统资源不泄露):
线程的主要目的是为了运行我们设计的函数,我们的函数应该在合适的时候被调用,这得要依赖于作业调度系统,同时在被调度之前需要进行一些初始化的动作,事实上线程的执行依赖于函数RtlUserThreadStart,初始化线程时直接将参数压入线程栈中并将ip寄存器指向了RtlUserThreadStart这样模拟了一个函数调用的过程,但是这个函数不能返回(因为栈中没有压入返回地址)系统以这样的方式初始化线程的执行环境并做调度。
VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr,PVOID pvParam)
{
_try{
ExitThread((pfnStartAddr)(pvParam));
}
_exception(UnHandledExceptionFilter(GetExceptionInformation())
{
ExitProcess(GetExceptionCode());
}
}
C运行时库在线程中运行的过程中依赖于_tiddata,_tiddata每个线程独立保存一份,若不存在在在调用c运行时库时会创建一份,但是RtlUserThreadStart中并没有处理这个结构的释放,所以不建议直接使用CreateThread函数,而使用_beginthreadex和_endthreadex来开始和结束线程。
线程被作业调度系统所调度,一般情况下无需干预,作业系统只会调度可调度的线程。可调度标识线程当前的状态,通常将线程的状态分为三种,运行(拥有CPU资源),等待(等待CPU资源),挂起(不可调度)(虽然还有可以分为创建和死亡的当我觉得没必要)。当一个线程被创建的时候认为是被挂起的(挂起计数为1),以便执行初始化之类的动作,初始化完成后若dwCreateFlags不为CREATE_SUSPENDED则将挂起技术设为0是的线程可以被调度。当然我们也可以通过Suspend和Resume函数来控制线程,当线程内部发生阻塞(如IO等待)时也会挂起。
当线程切换时系统使用CONTEXT结构记住线程的状态,这样线程在下一次获得CPU可以运行时,就可以从上此停止处继续。事实上,在Windows定义的所有结构中,CONTEXT结构是唯一一个特定于CPU的。
CONTEXT结构分为几个部分。CONTEXT_CONTROL包含CPU的控制寄存器,比如指令指针、栈指针、标志和函数返回地址。CONTEXT_INTEGER标识CPU的整数寄存器;CONTEXT_FLOATING_POINT标识CPU的浮点寄存器;CONTEXT_SEGMENTS标识CPU的段寄存器;CONTEXT_DEBUG_REGISTERS标识CPU的调试寄存器;CONTEXT_EXTENDED_REGISTERS标识CPU的扩展寄存器。Windows实际上允许我们查看线程的内核对象的内部,并获取当前CPU寄存器状态的集合,只需调用GetThreadContexts。在使用该函数前应该先调用SuspendThread;否则,系统可能正好获得调度此线程,这样一来,线程的上下文与所获取的信息就不一致了。一个线程实际上有两个上下文:用户模式和内核模式。该函数只能返回线程的用户模式上下文。GetThreadContext只能返回线程的用户模式上下文。如果调用SuspendThread暂停一个线程,但是该线程正在内核模式下执行,那么它的用户模式上下文保持不变,即使SuspendThread实际还没有暂停线程。线程恢复之前,不能再执行任何用户模式的代码。
Windows还允许我们通过调用SetThreadContext来改变结构中的成员,并把新的寄存器值放回到线程的内核对象中。同样如果要改变哪个线程的上下文,应该先暂停该线程,在调用该函数前,必须再次初始化CONTEXT的ContextFlags成员。
windows是抢占式系统,当一个CPU的时间片(大概20ms)执行完成之后系统将会把CPU资源分配给可调度的所有线程中优先级最高的线程,而一个高优先级线程从挂起恢复到等待的状态时若一个低优先级的线程正拥有CPU资源,则低优先级线程被暂停(变为等待)把CPU资源让给高优先级线程。线程创建的时候优先级通常为normal,我们可以通过Get/SetThreadPriority来改变线程的优先级,而系统通常也会对线程的优先级做一些调整。若要查询(是否)禁止调整线程的优先级则需要Get/SetThreadPriorityBoost,或者调用GetProcessPriorityBoost对进程中的所有线程进行统一设置。
除了作业系统调度线程执行之外我们还可以通过sleep和switchtothread来控制线程的执行,从而放弃剩下的时间片把CPU资源让给其他线程。
由于是系统是抢占式的所以要评估一个函数的执行效率或者执行时间时要排除掉被中断的时间,windows提供了运行时间的查看函数,这对于程序的性能分析十分有用。线程相关的直接对象是CPU的脉冲,再将脉冲换算为一个大概的时间,所以所获得的时间是不精确的,与时间相关的函数有GetTickCount和GetThreadTimes,虽然不完全精确但用于性能的统计是足够的。