线程基础----Windows核心编程

1、线程基础

      理解线程是至关重要的,因为每个进程至少都有一个线程。在本章,我们将讲述线程更多的细节。具体的说,我们将解释线程和进程的区别,它们各自有何职责。同时,还要解释系统如何使用 线程内核对象 来管理 线程。就像进程内核对象一样,线程内核对象也有属性,我们将探讨用于查询和更改这些属性的函数。此外,还要介绍可在进程中创建和生成更多线程的函数。

      线程有2个部分组成:

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

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

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

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

      由于线程需要的进程比进程小,所以建议你尽量使用额外的线程来解决编程问题,避免创建新的进程。但是,也不要把这个建议当做金科玉律。许多设计 更适合 用多个进程来实现。应该知道如何权衡利弊,让经验来指定你编程。

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

      每个计算机都有一个特别强大的资源:CPU。让CPU闲着是没有任何道理的(假设我们不考虑省电和散热问题)。为了让CPU保持“忙碌”,我们可以让它执行各种各样的任务。

2、何时不应该创建线程

      到目前为止,我们一直在为多线程应用程序高唱赞歌。尽管多线程应用程序好处很多,但仍有一些不足之处。有些开发人员认为,任何问题都可以通过把它分解成线程来解决。但是,这样想是大错特错的。

      线程能解决一些问题,但又会产生新的问题。一种常见的对线程的误用发生在开发应用程序的用户界面时。在几乎所有的应用程序中,所有用户界面组件(窗口)都应该共享同一个线程。一个窗口的所有子窗口无疑应该由一个线程来创建。有时,也允许在不同的线程上创建不同的窗口,但这类情形相当少见。

      通常,应用程序应该有一个用户界面线程,此线程负责创建所有窗口,另外,还有一个GetMessage循环。进程中的其它所有线程都是受计算能力的制约或者受I/O限制的工作线程,这些线程永远不会创建窗口。另外,用户界面线程的优先级通常高于工作线程。这样一来,用户界面才能迅速响应用户的操作。

3、编写第一个线程函数

      每个线程都必须有一个入口点函数,这是线程执行的起点。如果想在 进程中 创建辅助线程,它必须有自己的入口点函数,形式如下:

DWORD WINAPI ThreadProc( LPVOID lpParameter // thread data )

{

      DWORD dwResult=0;

      //...

      return dwResult;

}

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

关于线程函数应该注意下面几点:

1)在默认情况下,主线程的入口点函数必须命名为main、winmain等(除非我们用/ENTRY:链接器选项 来指定另一个函数作为入口点函数),与此不同的是,线程函数可以任意命名。事实上,如果应用程序中有多个线程函数,必须为它们指定不同的名称,否则编译器/链接器会认为你创建了一个函数的多个实现。

2)线程函数必须有一个返回值,它会成为该线程的退出代码。这类似于C/C++运行库的策略,令主线程的退出代码成为进程的退出代码。

3)线程函数(实际上包括所有函数)应该尽可能使用 函数参数 和 局部变量。使用静态变量和全局变量时,多个线程可以同时访问这些变量,这样可能会破坏变量中保存的内容。然而,由于函数的参数和局部变量是在线程栈上创建的。因此,不太可能被其它线程破坏。

4、CreateThread函数

      如果想要创建一个或多个辅助线程,只需要让一个正在运行的线程调用CreateThread函数即可。

HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
                                    SIZE_T dwStackSize, // initial stack size
                                    LPTHREAD_START_ROUTINE lpStartAddress, // thread function
                                    LPVOID lpParameter, // thread argument
                                    DWORD dwCreationFlags, // creation option
                                    LPDWORD lpThreadId // thread identifier
                                    );

当调用CreateThread时,系统会创建一个线程内核对象。这个线程内核对象不是线程本身,而是一个较小的数据结构,操作系统用这个数据结构来管理线程。可以把线程内核对象想象为一个 由线程统计信息 构成的小型数据结构。

      系统从进程的地址空间中分配内存给线程栈使用。新线程在与负责 创建 的那个线程在相同的进程上下文中运行。因此,新线程可以访问 进程内核对象的所有句柄、进程中的所有内存、同一个进程中其它所有线程的栈。这样一来,同一个进程中的多个线程可以很容易的互相通信。

4.1、lpThreadAttributes参数

      lpThreadAttributes参数是一个指向SECURITY_ATTRIBUTES结构的指针。如果想使用线程内核对象的默认安全属性,可以向此参数传入NULL(一般都会这样做)。如果希望所有 子进程 都能继承到这个线程对象的句柄,必须指定一个SECURITY_ATTRIBUTES结构,并将给结构的bInheritHandle成员初始化为true。

4.2dwStackSize参数

      该参数指定可以为线程栈使用多少地址空间。每个线程都拥有自己的栈。如果指定该参数为0,CreateThread会预定一个区域,并根据由 /STACK链接器开关 指定的 存储量来 调拨 存储器。 如果传入非0值,函数会为 线程 预定空间 并为之调拨所需的所有存储空间。由于所有存储器都已经事先调拨完毕,所以可以保证线程有指定的栈存储器可用。预定空间的大小要么由/STACK链接器开关来指定,要么由dwStackSize参数来指定,取其中较大的那一个。

4.3、lpStartAddress和lpParameter参数

      lpStartAddress参数指定 希望新线程 执行的函数的地址。lpParameter参数 是 在线程开始执行时将其传给线程函数。通过这个参数可以将一个初始值传给线程函数。这个初始值可以是一个数值,也可以是指向一个数据结构的指针(其中包含额外的信息)。

      创建多个线程时,可以让它们使用同一个函数地址作为起点。这样做完全合法,而且非常有用。例如:我们这样实现一个WEB服务器程序,令其为每个客户端请求 创建一个新的线程,并分别对每个请求进行处理。每个线程都知道自己正在处理哪个客户端的请求,因为,在创建每个线程的时候,都向其传递了不同的lpParameter参数。

      记住,Windows是一个 抢占式的 多线程系统(preemptive multithreading system)。这意味着新的线程和 调用CreateThread函数的线程可以同时执行。因为2个线程是同时运行的,所以可能出现问题。例如:

DWORD WINAPI FirstThread(LPVOID pvParam)

{

      int x=0;

      DWORD dwThreadID;

      HANDLE hThread=CreateThread(NULL,0,PVOID(&x),0,&dwThreadID);//创建新的线程

      CloseHandle(hThread);//关闭线程句柄,因为我们不再引用新线程

      return 0;

}

DWORD WINAPI SecondThread(LPVOID pvParam)

{

      *( (int*)pvParam )=5;

      return 0;

}

上面的代码中,FirstThread 可能会 在SecondThread函数将5赋值给x之前完成任务。如果发生这种情况,SecondThread不知道FirstThread已经不存在了,所有会试图更改现已无效的地址的内容。这会导致SecondThread产生违规访问,因为FirstThread的栈已经在线程终止时被销毁。解决这个问题的一个方法是x声明为一个静态变量,使编译器在应用程序的数据段中为x创建一个存储区域,而不是在线程栈中。

      但是,这会使函数不可重入。换言之,不能创建2个线程来执行同一个函数,因为这两个线程将共享同一个静态变量。为了解决这个问题,另一个办法是使用正确的线程同步技术。

4.4、dwCreationFlags

      dwCreationFlags参数指定标志来控制线程的创建,他可以是下面2个值之一。如果值为0,线程创建之后立即运行。如果值为CREATE_SUSPENDED,系统将创建并初始化线程,但是会暂停该线程的执行,这样他就无法进场调度。在线程开始运行之前可以用这个标志暂停执行,然后修改线程的一些属性。由于很少有必要这样做,所以该标志并不常用。

4.5、lpThreadId 

      这是该函数的最后一个参数。它必须是DWORD的一个有效地址,用它来存储系统分配给新线程的ID,可以为这个参数传递NULL(一般都是这样做的),它告诉函数我们对线程ID没兴趣。

5、终止线程的运行

      线程可以通过下面4种方法来终止运行。

1)线程函数返回(这是强烈推荐的)。

2)线程通过调用ExitThread函数杀死自己(要避免使用这种方法)

3)同一个进程或另一个进程中的线程调用TerminateThread函数(要避免使用这种方法)

4)包含线程的进程终止(要避免使用这种方法)

5.1、线程函数返回

      设计线程函数时,应该确保 在我们希望线程终止运行时,就让它们返回。这是保证线程的所有资源都被正确清理的唯一方式。让线程函数返回,可以确保以下正确的应用程序清理工作都得以执行。

@线程函数中所创建的所有C++对象都通过其析构函数被正确销毁

@操作系统正确释放线程栈使用的内存

@操作系统把线程的退出代码(在线程的内核对象中维护)设为线程函数的返回值

@系统递减线程内核对象的使用计数

5.2ExitThread函数

      为了强迫线程终止运行,可以让它调用ExitThread函数。VOID ExitThread(DWORD dwExitCode);该函数将终止线程的运行,并将导致操作系统清理该线程使用的所有操作系统资源。但是,你的C/C++资源(如C++类对象)不会被销毁。更好的做法是直接从线程函数返回,不要自己调用ExitThread函数。

      当然,可以使用ExitThread的dwExitCode参数来告诉系统将线程的退出代码设为什么。ExitThread函数没有返回值,因为线程已终止,而且不能执行更多的代码。

5.3、TerminateThread函数

      调用TerminateThread函数也可以“杀死”一个线程,如下所示:BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);不同于ExitThread总是“杀死”主调线程,TerminateThread能“杀死”任何线程。hThread参数标识了要终止的那个线程的句柄。线程终止运行时,其退出代码为你传递的dwExitCode参数值。同时,线程的内核对象的使用计数会递减。

说明:TerminateThread函数是异步的。也就是说,它告诉系统你想终止线程,但在函数返回时,并不保证线程已经终止了。如果需要确定线程已终止运行,还需要调用WaitForSingleObject或类似的函数,并向其传递线程句柄。

      一个设计良好的应用程序绝不会使用这个函数,因为被终止运行的线程收不到它被“杀死”的通知,线程无法正确清理,而且不能阻止自己被终止运行。

5.4、线程终止运行时

      线程终止运行时,会发生下面这些事情。

@线程拥有的所有 用户对象句柄 会被释放。在Windows中,大多数对象是由 包含了“创建这些对象的线程”的进程 拥有的。一个线程有2个用户对象:窗口(window)和挂钩(hook)。一个线程终止运行时,系统会自动销毁由线程创建或安装的任何窗口,并卸载由线程创建或安装的任何挂钩。其它对象只有在拥有线程的进程终止时才被销毁。

@线程的退出代码从STILL_ACTIVE变成传给ExitThread或TerminateThread的代码。

@线程内核对象的状态变为触发状态

@如果线程是进程中的最后一个活动线程,系统认为进程也终止了

@线程内核对象的使用计数减1

线程终止运行时,其关联的线程对象不会自动释放,除非对这个对象的所有未结束的引用都被关闭了。

      一旦线程不再运行,系统中就没有别的线程再用该线程的句柄了。但是,其它线程可以调用GetExitCodeThread来检查hThread所标识的那个线程是否已经终止运行,如果已终止运行,可判断其退出代码是什么:BOOL GetExitCodeThread(HANDLE hThread,PDOWRD  pdwExitCode);退出代码的值通过pdwExitCode指向的DWORD来返回。如果在调用GetExitCodeThread时,线程尚未终止,函数就用STILL_ACTIVE标识符来填充DWORD.如果函数调用成功,就返回true。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值