【Windows核心编程学习札记】线程

【Windows核心编程学习笔记】线程

一、什么是线程

 

线程,有时候被称为轻量级进程(LightWeight Process,LWP),是程序执行的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

线程由两部分组成:

(1)线程的内核对象,操作系统用它来管理线程以及存放线程统计信息。

(2)线程栈,用于维护线程执行时所需的所有函数参数和局部变量。

 

进程从来不执行任何东西,它只是一个线程的容器。线程必然是在某个进程的上下文中创建的,而且会在这个进程内部“终其一生”。这意味着要在其进程的地址空间内执行代码和处理数据。所以,假设一个进程上下文中有两个以上的线程在运行,这些线程将共享一个地址空间。这些线程可以执行同样的代码,可以处理相同的数据。此外,这些线程还共享内核对象,因为句柄表示针对每一个进程的,而不是针对每一个线程。

相较于线程,进程所使用的系统资源更多。其原因在于地址空间。为一个进程创建一个虚拟的地址空间需要大量系统资源。系统中会发生大量的记录活动,而这需要大量内存。而且,由于exe和dll文件要加载到一个地址空间,所以还需要用到文件资源。另一方面,线程使用的资源要少很多。事实上,线程只有一个内核对象和一个栈,几乎不涉及记录活动。

 

二、线程的创建

 

每次初始化进程时,系统会创建一个主线程。对于用Microsoft C/C++编译器生成的应用程序,这个线程会执行C/C++运行库的启动代码,后者调用入口点函数(_tmain或_tWinMain),并继续执行,直至入口点函数返回C/C++运行库的启动代码,后者最终调用ExitProcess。对于许多应用程序来说,这个主线程是i应用程序唯一需要的线程,但是进程也可以创建额外的线程来帮助他们完成自己的工作。

 

每个线程都有一个入口点函数,这是线程执行的起点。形式如下:

DWORD WINAPI ThreadFunc(PVOID pvParam)
{
         DWORDdwResult = 0;
         ...
         returndwResult;
}

线程函数可以执行我们希望它指向的任何任务。最终,线程函数将终止运行并返回。此时,线程将终止运行,用于线程栈的内存也会被释放,线程内核对象的使用记数也会递减。如果使用记数变为0,那么线程内核对象会被销毁。类似于进程内核对象,线程内核对象至少可以达到相关联的线程那样长。不过对象的寿命可能超过线程本身的寿命。

关于线程函数需要注意一下几点:

  1. 线程函数可以任意命名,但是如果应用程序中有多个线程函数,必须指定不同的名称。
  2. 线程函数必须返回一个值,它会成为线程的退出代码。
  3. 线程函数应该尽可能使用函数参数和局部变量。使用静态变量和全局变量,多个线程可以同时访问,可能会破坏变量中保存的内容。

 

要创建线程,可以调用函数CreateThread:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  DWORD dwStackSize,
  LPTHREAD_START_ROUTINE lpStartAddress,
  LPVOID lpParameter,
  DWORD dwCreationFlags,
LPDWORDlpThreadId);

CreateThread函数调用时,系统会创建一个线程内核对象,它是一个由线程统计信息构成的小型数据结构。操作系统用这个结构管理线程。

新线程与负责创建的那个线程在相同的进程上下文中运行,因此新线程可以访问进程内核对象的所有句柄、进程中的所有内存以及同一个进程中其他所有线程的栈。这样一来,同一个进程中的多个线程可以很容易地相互通信。


各参数解释:

lpThreadAttributes:线程安全属性

dwStackSize:指定线程可以为其线程栈使用多少地址空间。每个线程都有自己的栈。

lpStartAddress:指定希望新线程执行的线程函数的地址。线程函数的参数lpParameter与最初传递给CreateThread函数的lpParameter参数是一样的。

创建多个线程时,可以让他们使用同一个函数地址作为起点。

Windows是一个抢占式的多线程系统(preemptivemultithreading system),这意味着新线程可以和调用CreateThread函数的线程同时执行。

dwCreationFlags:指定额外的标志来控制线程的创建。如果值为0,线程创建之后立即可以进行调度。如果值为CREATE_SUSPENDED,系统将创建并初始化线程,但是会暂停该线程的运行,这样就无法进行调度。

lpThreadId:存储系统分配给新线程的ID。

 

三、终止运行的线程

 

线程可以通过四种方法来终止运行:

  1. 线程函数返回(强烈推荐)。
  2. 线程通过调用ExitThread函数杀死自己(避免使用)。
  3. 同一个进程或另一个进程中的线程调用TerminateThread函数(避免使用)。
  4. 包含线程的进行终止运行(避免使用)。

设计线程函数时,应该确保在我们希望线程终止运行时,就让他们返回,这是保证资源被正确清理的唯一方式。

 

让线程函数返回可以确保一下正确的应用程序清理工作都得以执行:

  1. 线程函数中创建的所有C++对象都通过其析构函数被正确销毁。
  2. 操作系统正确释放线程栈使用的内存。
  3. 操作系统将线程的退出码设置为线程函数的返回值。
  4. 系统减少线程的内核对象的使用记数。

 

四、线程内幕

 

下图展示了如何创建和初始化一个线程:

【Windows核心编程学习札记】线程

对CreateThread函数的一个调用导致系统创建一个线程内核对象。该对象的最初的使用记数为2.(除非线程终止,而且从CreateThread返回的句柄被关闭,否则线程的内核对象不会被销毁)。该内核对象的其他属性也被初始化:暂停记数被设为1,退出代码被设为STILL_ACTIVE,而且对象被设为未触发状态。

一旦创建了内核对象,系统就分配内存,供线程的堆栈使用。此内存时从进程的地址空间内分配的,因为线程没有自己的地址空间。然后系统将两个值写入新线程的堆栈的最上端(注意:线程堆栈始终是从高位地址到地位内存地址构建的),写入线程堆栈第一个值是传给CreateThread函数的lpParameter参数值。紧接在下方的是传给这个函数的lpStartAddress值。

每个线程都有自己的一组CPU寄存器,成为线程的上下文(context)。上下文反应了当线程上一次执行时,线程的CPU寄存器状态。线程的CPU寄存器全部保存在一个CONTEXT结构中,这个结果保存在线程的内核对象中。

指令指针寄存器和栈指针寄存器是线程上下文中最重要的两个寄存器。这两个地址标识的内存都位于线程所在进程的地址空间中。当线程内核对象被初始化的时候,CONTEXT结果的堆栈指针继承前辈设为lpStartAddress在线程堆栈中的地址。而指令指针寄存器被设为函数RtlUserThreadStart的地址。