文章目录

  • 1. 何时创建线程
  • 2. 何时不应该创建线程
  • 3. 编写第一个线程函数
  • 4. CreateThread函数
    • 4.1 lpThreadAttributes参数
    • 4.2 dwStackSize参数
    • 4.3 lpStartAddress 和 lpParameter参数
    • 4.4 dwCreationFlags参数
    • 4.5 lpThreadId参数
  • 5. 终止运行线程
    • 5.1 线程返回函数
    • 5.2 ExitThread函数
    • 5.3 TerminateThread函数
    • 5.4 进程终止运行时
    • 5.5 线程终止运行时

理解线程是至关重要的,因为每个进程至少都有一个线程。

线程也有两个组成部分:

  • 一个是线程的内核对象,操作系统用它管理线程。系统还用内核对象来存放线程统计信息的地方。

  • 一个线程栈,用于维护线程执行时所需的所有函数参数和局部变量。(在第16章中,将详细讨论系统如何管理线程栈。)

假如一个进程上下文中有两个以上的线程运行,这些线程将共享同一个地址空间。这些线程可以执行同样的代码,哥以处理相同的数据。此外,这些线程还共享内核对象句柄,因为句辆表是针对每一个进程的而不是针对每一个线程。

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

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

在深入讨论线程之前,稍微花一点时间来讨论如何在应用程序的架构中正确地使用线程。

1. 何时创建线程

线程描述了进程内部的一条执行线路。每次初始化进程时,系统都会创建一个主线程。

将一个应用程序设计成多线程的,可以使该应用程序更易于扩展。如下一章所述,每个线程都被分配了一个CPU。所以,如果我计算机有两个CPU,而且应用程序有两个线程,那么两个CPU都会很忙。其结果就是,只需花一个任务的时间,两个任务都可以完成。

因为每个进程内部至少有一个线程,所以即使我们不在应用程序中进行特殊处理,也能从多线程操作系统获益。例如,可以同时生成应用程序和使用字处理软件(我经常这样做)。如果计算机有两个CPU,那么生成过程将在一个处理器上进行,另一个处理器则负责处理文档。换言之,用户感觉不到性能有所下降,而且在他进行输入的时候,用户界面也不会无法响应。此外,如果编译器的一个缺陷导致其线程进入无限循环,其他进程仍可运行。

2. 何时不应该创建线程

有些开发人员认为,任何问题都可以通过把它分解成线程来解决。但是,这样想是大错特错的。

在开发应用程序的用户界面时。在几乎所有应用程序中,所有用户界面组件(窗口)都应该共享同一个线程。一个窗口的所有子窗口无疑应该由一个线程来创建。

用户界面线程的优先级通常高于工作线程。这样一来,用户界面才能迅速响应用户的操作。

我们应该合理使用多线程。不要因为能用就盲目的用。即使只用进程的主线程,能能写出许多有用而且功能强大的应用程序。

3. 编写第一个线程函数

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

DWORD WINAPI ThreadFunc(PVOID pvParam)
{DWORD dwResult = 0;...return (dwResult);
}

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

关于线程函数,要注意以下几点:

  • 在默认情况下,主线程的入口点函数必须命名为main,wmain,WinMain 或 wWinMain。与此不同的是,线程函数可以任意命名。事实上,如果应用程序中有多个线程函数,必须为他们指定不同的名称,否则编译器/链接器会认为你创建了一个函数的多个实现。
  • 因为主线程的入口点函数有字符串参数,所以它提供了ANSI/Unicode版本供我们选择:mian/Wmain 和 WinMain/wWinMain。相反,线程函数只有一个参数,而且其意义由我们(而非操作系统)来定义。因此,我们不必担心ANSI/Unicode问题。
  • 线程函数必须返回一个值,它会成为该线程的退出代码。这类似于C/C++运行库的策略:令主线程的退出代码成为进程的退出代码。
  • 线程函数(实际包括所有函数)应该尽可能使用函数参数和局部变量。使用静态变量和全局变量时,多个线程可以同时访问这些变量,这样可能会破坏变量中保存的内容。然而,由于函数的参数和局部变量是在线程栈上创建的。因此,不太可能被其它线程破坏。

4. CreateThread函数

官方文档:https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createthread

如果想创建一个或多个辅助线程,只需让一个正在运行的线程调用CreateThread:

HANDLE CreateThread(LPSECURITY_ATTRIBUTES   lpThreadAttributes,SIZE_T                  dwStackSize,LPTHREAD_START_ROUTINE  lpStartAddress,__drv_aliasesMem LPVOID lpParameter,DWORD                   dwCreationFlags,LPDWORD                 lpThreadId
);

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

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

说明:

CreateThread函数是用于创建线程的Windows函数。不过,如果写的是C/C++代码,就绝对不要调用CreateThread。相反,正确的选择是使用Microsoft C++运行库函数 _beginthreadx。如果使用的不是Microsoft C++编译器,你的编译器的提供商应该提供类似的函数来替代CreateThread。不管这个替代函数是什么,都必须使用它。

4.1 lpThreadAttributes参数

一般设置为NULL

4.2 dwStackSize参数

一般设置为0

4.3 lpStartAddress 和 lpParameter参数

lpStartAddress 参数指定希望新线程执行的线程函数的地址。

lpParameter表示传入函数的参数。

Windows是一个抢占式的多线程系统。这意味着新的线程和调用CreateThread函数的线程可以同时执行。因为两个线程是同时运行的,所以可能出现问题。

4.4 dwCreationFlags参数

dwCreationFlags参数指定额外的标志来控制线程的创建。他可以是两个值之一。

  • 如果值为0

    线程创建之后立即就可以进行调度。

  • 如果值为CREATE_SUSPENDED

    系统将创建并初始化线程,但是会暂停该线程的运行,这样她就无法进行调度。

4.5 lpThreadId参数

可以为这个参数传递NULL(一般都是这样做的),他告诉函数我们对线程ID没有兴趣。

5. 终止运行线程

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

  • 线程函数返回(这是强烈推荐的)。
  • 线程通过调用ExitThread函数“杀死”自己(要避免使用这种方法)。
  • 同一个进程或另一个进程中的线程调用TerminateThread函数(要避免使用这种方法)。
  • 包含线程的进程终止运行(这种方法避免使用)。

5.1 线程返回函数

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

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

  • 线程函数中创建的所有C++对象都通过其析构函数被正确销毁。
  • 操作系统正确释放线程栈使用的内存。
  • 操作系统把线程的退出代码(在线程的内核对象中维护)设为线程函数的返回值。
  • 系统递减少线程的内核对象的使用计数。

5.2 ExitThread函数

为了强迫线程终止运行,可以让它调用ExitThread:

void ExitThread(DWORD dwExitCode
);

该函数将终止线程的运行,并导致操作系统清理该线程使用的所有操作系统资源。但是,你的C/C++资源(如C++类对象)不会被销毁。有鉴于此,更好的做法是直接从线程函数返回,不要自己调用ExitThread函数。

说明:

终止线程运行的推荐方法是让它的线程函数返回。但是,注意ExitThread函数是用于“杀死”线程的Windows函数。如果要写C/C++代码,就绝对不要调用ExitThread。相反,应该使用C++运行库函数_endthreadex

5.3 TerminateThread函数

调用TerminateThread函数也可以“杀死”一个线程。

BOOL TerminateThread(HANDLE hThread,DWORD  dwExitCode
);

不同于ExitThread总是“杀死”主调线程(calling thread),TerminateThread能“杀死”任何线程。hThread参数标识了要终止的哪个线程的句柄。线程终止运行时,其退出代码将变成你作为dwExitCode参数传递的值。同时,线程的内核对象的使用计数会递减。

注意:

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

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

说明:

如果通过返回或调用ExitThread函数的方式来终止一个线程的运行,该线程的堆横也会被销毁。但是,如果使用的是TerminateThread,那么除非拥有此线程的进程终止运行,否则系统不会销毁这个线程的堆栈。Microsoft 故意以这种方式来实现Terminaterhread.否则,假如其他还在运行的线程要引用被“杀死”的那个线程的推械上的值,就会引起访问违规。让被“杀死”的线程的推概保留在内存中,其他的线程就可以继续正常运行。

5.4 进程终止运行时

ExitProcess和TerminateProcess也可用于终止线程的运行。区别在于,这些函数会使终止运行的进程中的所有线程全部终止。同时,由于整个进程都会关闭,所以它所使用的所有资源肯定都会被清理。其中必然包括所有线程的堆栈。这两个函激全导致进程中剩余的所有线程被强行“杀死”,这就好像是我们为剩余的每个线程都调用了Terminaterread。显然,这意味着正确的应用程序潜理工作不会执行:C++对象的析构函数不会被调用,数据不会回写到磁盘。。。

5.5 线程终止运行时

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

  • 线程内核对象的状态变为触发状态。
  • 如果线程是进程中的最后一个活动线程,系统认为进程也终止了。
  • 线程内核对象的使用计数递减1。

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

一旦线程不再运行,系统就没有别的线程再用该线程的句柄了。但是其它线程可以调用GetExitCodeThread来检查hThread所表示的那个线程是否已终止运行;如果已终止运行,可判断其退出代码是什么:

BOOL GetExitCodeThread(HANDLE  hThread,LPDWORD lpExitCode
);

《windows核心编程》第6章 线程基础相关推荐

  1. Windows核心编程 第六章 线程基础知识 (上)

    第6章 线程的基础知识 理解线程是非常关键的,因为每个进程至少需要一个线程.本章将更加详细地介绍线程的知识.尤其是要讲述进程与线程之间存在多大的差别,它们各自具有什么作用.还要介绍系统如何使用线程内核 ...

  2. Windows核心编程 第六章 线程基础知识 (下)

    6.6 线程的一些性质 到现在为止,讲述了如何实现线程函数和如何让系统创建线程以便执行该函数.本节将要介绍系统如何使这些操作获得成功. 图6 - 1显示了系统在创建线程和对线程进行初始化时必须做些什么 ...

  3. Windows核心编程 第十一章 线程池的使用

    第11章 线程池的使用 第8章讲述了如何使用让线程保持用户方式的机制来实现线程同步的方法.用户方式的同步机制的出色之处在于它的同步速度很快.如果关心线程的运行速度,那么应该了解一下用户方式的同步机制是 ...

  4. Windows核心编程 第七章 线程的调度、优先级和亲缘性(下)

    7.6 运用结构环境 现在应该懂得环境结构在线程调度中所起的重要作用了.环境结构使得系统能够记住线程的状态,这样,当下次线程拥有可以运行的C P U时,它就能够找到它上次中断运行的地方. 知道这样低层 ...

  5. Windows核心编程 第七章 线程的调度、优先级和亲缘性(上)

    第7章 线程的调度.优先级和亲缘性 抢占式操作系统必须使用某种算法来确定哪些线程应该在何时调度和运行多长时间.本章将要介绍Microsoft Windows 98和Windows 2000使用的一些算 ...

  6. 《Windows 核心编程》27章:硬件输入模型和局部输入状态

    内容概括 本章主要讨论系统的硬件输入模型 重点考察:按键和鼠标事件进入系统.发送给适当的窗口 设计输入模型的目标:保证一个线程的动作不影响其他线程 16位 Windows 中的案例: 若一个任务引起死 ...

  7. Windows核心编程 第27章 硬件输入模型和局部输入状态

    第27章 硬件输入模型和局部输入状态 这章说的是按键和鼠标事件是如何进入系统并发送给适当的窗口过程的.微软设计输入模型的一个主要目标就是为了保证一个线程的动作不要对其他线程的动作产生不好的影响. 27 ...

  8. Windows核心编程 第十七章 -内存映射文件(下)

    17.3 使用内存映射文件 若要使用内存映射文件,必须执行下列操作步骤: 1) 创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件. 2) 创建一个文件映射内核对象,告诉系统该 ...

  9. Windows核心编程 第十七章 -内存映射文件(上)

    第1 7章 内存映射文件 对文件进行操作几乎是所有应用程序都必须进行的,并且这常常是人们争论的一个问题.应用程序究竟是应该打开文件,读取文件并关闭文件,还是打开文件,然后使用一种缓冲算法,从文件的各个 ...

  10. Windows核心编程 第四章 进程(上)

    第4章 进 程     本章介绍系统如何管理所有正在运行的应用程序.首先讲述什么是进程,以及系统如何创建进程内核对象,以便管理每个进程.然后将说明如何使用相关的内核对象来对进程进行操作.接着,要介绍进 ...

最新文章

  1. javascript 变量监听器
  2. opencv_4.5.0/OpenCvSharp4.0 九点标定
  3. 嵌入式软件设计第12次实验报告-140201235-陈宇
  4. VTK:图片之ImageRFFT
  5. 前端面试题集结号之CSS
  6. SAP云平台测试帐号如何进入Neo环境
  7. ubuntu18.04 用xhell连接失败
  8. android动画框架,GitHub - azhengyongqin/CustomAnimationFramework: Android自定义曲线路径动画框架...
  9. 二分法实战教学快速入门(折半查找法)
  10. 使用Kotlin的Android TextView –全面教程
  11. Corona Enterprise 引入第三方 jar 包
  12. android apk 对应目录,android 如何预置APK到 data 和system/app目录
  13. 继承ActionSupport类
  14. AE CC2018/64位安装步骤及各种出现问题!!(真的装了好久)
  15. Matlab画柱状图和饼状图以及横纵坐标设置,宽度设置等
  16. lwip 动态修改IP
  17. c语言 电话簿 链表,C语言_链表_结构体_电话簿简单实现
  18. 全面理解Gradle - 定义Task
  19. python比赛积分类算法题_python几道简单的算法题
  20. 手机LBS位置应用服务盘点

热门文章

  1. LoadLibrary动态加载dll失败,GetLastError返回错误码126
  2. Oracle9i的1467错误
  3. 亚马逊欧洲站的VAT需要多久申报一次
  4. SSL证书错误了怎么办?
  5. 首届中国城市轨道交通科技创新创业大赛清华站复赛成功举行
  6. 腾讯汤道生:开放中台能力助力产业升级
  7. nu.xom:Serializer
  8. 关于音频EQ、DRC、等响度、3D环绕音、虚拟低音、变音、AEC、AGC、ANS等解释
  9. f5服务器维护,F5-LTM(服务器的负载均衡)网络结构和功能
  10. png转bmp,png批量转成bmp