线程也是有两部分组成的:

  1. 线程的内核对象,操作系统用来管理线程和统计线程信息的地方。
  2. 线程堆栈,用于维护现场在执行代码的时候用到的所有函数参数和局部变量。

  进程是线程的容器,如果进程中有一个以上的线程,这些线程将共享进程的地址空间,操作空间中的数据,执行相同的代码,对相同的数据操作,甚至内核对象句柄(因为它是依托进程而不是线程存在的)。

  所以进程使用的系统资源比线程多的多,线程只需要一个内核对象和一个堆栈。既然线程比进程需要的开销少,因此始终都应该设法用增加线程来解决编程问题。当然这也不是一成不变的,应该懂得权衡利弊。



何时运行线程

  前面的章节已经提到过,当进程被初始化时,系统就要为进程创建一个主线程,用以和CC++运行期库的启动代码一起运行,然后进入入口点函数,然后运行至入口点函数返回并CC++运行期库的启动代码调用ExitProcess为止。

  每个计算机都有一个功能强大的资源,CPU。让它闲置起来是没有道理的,应该让它处于繁忙之中,执行各种各样的工作:

  • 打开Windows2K开始就自带的内容索引服务程序,它能创建一个低优先级的线程,以定期打开磁盘上的文件内容并为之做索引。这样可以大大加快查找文件的效率。
  • 还可以使用磁盘碎片整理软件,使用低优先级线程运行,在系统空项期整理文件碎片。
  • 自动编译源代码文件、实时查看错误和警告信息。
  • 电子表格应用程序能够在后台运算。
  • 文字处理程序能够执行重新分页、拼写、后台打印和语法检查。
  • 文件内容后台拷贝到其他介质中。
  • Web浏览器和其他的服务器通信。

  最重要的是,多线程可以简化用户界面。设计一个拥有多线程的应用程序,可以扩大应用程序的功能。每一个线程都被分配了一个cpu,因此如果计算机有多个CPU,就可以让所有cpu都处于繁忙状态。


不能创建线程的情况

  多线程并发至少有一个问题:代码的重入,数据访问的冲突。

  一般来说一个应用程序有一个用户界面线程用于创建所有窗口,以及一个GetMessage循环。其他的线程都是工作线程,比用户界面线程优先级要低。


编写第一个线程函数

  主线程有对应的入口点,那么线程函数也有个入口函数,作为开始执行第一条代码的地方。和主线程一样,执行代码到结束就释放资源,线程内核对象的引用计数递减。

  • 和主线程不同,线程函数可以是任何名字。
  • 线程函数也可以传递参数。
  • 线程函数必须返回一个值,作为退出代码。
  • 线程函数应该尽可能地使用函数参数和局部变量。如果访问静态和全局的数据,就会有同步的问题。

CreateThread函数 

  调用下面的WindowsAPI就可以创建一个线程:

  

  上面的函数被调用的时候,系统创建一个内核对象,用来管理与线程相关的数据结构,和进程是类似的。系统从进程的地址空间中分配内存,给线程的堆栈使用。新线程的运行环境与创建线程的环境相同。这样使得同一个进程的多个线程之间的通信时相对方便的。

  注意:CreateThread函数是Windows用来创建线程的函数。可是如果你在写CC++代码的话,就不应该使用它了,而是使用VisualC++的运行时函数_beginthreadex。如果不是VisualC++编译器,你的编译器供应商会提供CreateThread的替代函数。反正不能用CreateThread。

  psa参数是指向SECURITY_ATTRIBUTES结果的指针,一般使用默认值。具体的用法请看第三章。

  cbStack参数用于设置线程堆栈大小,可以用链接程序的/STACK:[reserve][.commit]开关控制这个值。reserve参数用来设定系统为线程堆栈保留的地址控件量,默认1MB;commit参数用于设定“应该承诺用于堆栈保留区的物理存储器的容量”,默认值是1页。这个参数即便传递了值,函数仍然会检查链接器已经设置的值,哪个大用哪个。如果把0传进来,就使用Stack开关中设置的值。无论如何,这个值应该有个上限,否则如果存在递归则有可能消耗完所有的资源。

  pfnStartAddrpvParam分别是入口地址和参数。

  fdwCreate参数可以是0或者CREATE_SUSPEND。前者表示线程创建后立即调度,后者表示创建线程后先暂停运行。然而后者并不常用。

  pdwThreadID参数用来存放线程ID。

  


终止线程的运行  

  • 线程函数返回(最好的办法)如果线程可以返回,就可以确保下列事项的实现:
  1. 在现场函数中创建的所有C++对象都能通过它们的析构函数销毁。
  2. 操作系统正确地释放线程堆栈使用的内存。
  3. 系统将线程的退出代码(在线程的内核对象中维护)设置为线程函数的返回值。
  4. 系统递减线程内核对象的引用计数。
  • 调用ExitThread函数,线程自行撤销(最好不用)。如果使用,C++资源将不会被回收。实在要用也是用VisualC++提供的_endthreadex,或者你的编译器供应商提供的替代函数。
  • 使用TerminateThread函数(避免使用)。它能撤销线程,线程的内核对象的引用计数也会递减。
  1. TerminateThread函数是异步运行的,想知道线程终止运行,就要调用WaitForSingleObject或者类似的函数。(设计良好的应用程序从来不使用这个函数,因为被终止运行的线程收不到它被撤销的通知,线程不能被正确地清除,并不能防止自己被撤销)
  2. 使用这个函数,系统是不回收这个线程的堆栈资源的。
  3. 线程终止运行的话,DLL通常接受清楚通知。而使用这个函数,DLL就不接受通知了,这就阻挡了适当的清除。
  • 包含线程的进程终止运行(避免使用)
  1. 就像对剩余的每个线程调用TerminateThread一样。显然这意味着正确的应用程序清除没有发生:C++对象的析构函数没有被调用, 数据没有转至磁盘等。

线程如果终止

  • 线程拥有的用户对象全被释放。
  • 线程的退出代码从STILL_ACTIVE传递给ExitThread或者TerminateThread
  • 线程的内核对象变为已通知。
  • 如果是最后一个线程,系统也将进程视为已经终止运行。
  • 线程内核对象的引用计数递减1。

一旦线程不再运行,系统中就没有别的线程能够处理这个线程的句柄,而别的线程可以调用GetExitCodeThread来检查hThread标识的线程是否时间终止运行。如果是这样,就确定它的退出代码:

  

  如果尚未终止运行,函数就返回STILL_ACTIVE标识符(定义为0x103),放进pdwExitCode。如果成功就返回TRUE


线程的其他性质

  SP和IP分别是参数和线程函数的入口!

  

  如果调用CreateThread创建了一个内核对象,下面的事情就会发生:

  1. 这个对象的引用计数是2。
  2. 线程的内核对象的其他属性也被初始化,引用计数被设置为1,退出代码设置为STILL_ACTIVE(0x103),该对象的已通知设置为未通知状态。

  一旦内核对象创建完成:

  1. 系统就从进程的地址空间中,给线程堆栈分配内存。
  2. 系统先后把参数和入口地址写入堆栈。

  每个线程都有自己的一套CPU寄存器,成为线程的上下文,用以反映线程上次运行时寄存器的状态。这些寄存器保存在CONTEXT结构里,这个结构本身则保存在线程的内核对象中。

  ESP和EIP是线程上下文中两个最重要的寄存器。由于线程总是在进程的上下文中运行的,因此这两个寄存器是用于标识线程所在的进程的地址空间中的。

  在线程被初始化的时候,context结构的堆栈指针寄存器暂存于线程堆栈上用来放置pfnStartAddr的地址,由一个未文档化的函数BaseThreadStart(在Kernel32.dll模块)调用:

  

  当线程完全初始化后,系统就要查看CREATE_SUSPEND标识是否被传递到CreateThread。如果没有传递,系统就把线程的暂停计数递减为0,线程就可以调度在一个进程中,然后加上上次保存的上下文加载到寄存器,线程就开始执行代码。

  根据上图,我们可以知道BaseThreadStart函数才是真正开始执行线程函数的地方,这个函数有两个参数,BaseThreadStart认为是被另一个函数调用的,因为它可以访问两个参数,但实际情况并非如此。正是因为可以访问这些参数,是因为操作系统显式地将值写入了堆栈(参数传递给函数的地方)。不过也有CPU使用寄存器传递参数,这样的话系统会允许线程在执行BaseThreadStart之前对寄存器初始化。

  如果新线程开始执行BaseThreadStart函数,将会发生下列情况:

  • 在线程函数中建立一个结构化异常处理(SEH)帧,线程在发生任何异常的时候都会得到系统的默认处理。
  • 系统调用线程函数,并将你传递给CreateThreadpvParam参数传递给它。
  • 当线程返回时,BaseThreadStart调用ExitThread,并将返回值传给它。线程内核对象的引用计数递减,线程停止执行。
  • 如果线程产生了一个没有处理的异常条件,则由BaseThreadStart函数建立的SEH帧处理它。这样最终的结果是停止进程的运行,而不只是线程。

  BaseThreadStart函数中,要么调用ExitThread,要么调用ExitProcess。这意味着线程不能退出这个函数,而是把返回地址推进堆栈,这样线程就知道在哪里返回。但是BaseThreadStart函数没有返回值,这样如果它不撤销线程,而是试图返回,那么就会访问到一个随机的内存地址,从而引发违规。

  当进程的主线程被初始化的时候,它的指令指针被设置为另一个未文档化的函数BaseProcessStart,结构并不陌生:

  

VOID BaseProcessStart(PROCESS_START_ROUTINE pfnStartAdr){
__try{
ExitThread(pfnStartAddr());
}
__except(UnhandleExceptionFilter(GetExceptionInformation())){
ExitProcess(GetExceptionCode());
}
}

  两个函数之间唯一的差别就是,BaseProcessStart没有使用pvParam参数。当BaseProcessStart开始执行时,它调用CC++运行时库的启动代码,接着初始化对应的入口点函数并调用它们。当入口点函数返回时,CC++运行时库的启动代码就调用ExitProcess。因此,对于CC++应用函数来说,主线程从不返回BaseProcessStart函数。


  VisualC++有6个CC++运行时库:

  • LibC.lib,单线程应用程序的静态链接库(创建程序时默认使用)
  • LibCD.lib,单线程应用程序的静态链接库的调试版
  • LibCMt.lib,多线程应用程序的静态链接库的发行版
  • LibCMtD.lib,多线程应用程序的静态链接库的调试版。
  • MSVCRt.lib,动态链接MSVCRt.lib的发行版的输入库。
  • MSVCRtD.lib,动态链接MSVCRtD.lib的调试版的输入库,同时支持单线程和多线程。

  无论实现什么类型的编程项目,必须知道哪个库将链接到你的项目。在CC++选项卡的Code Generation显示的类别中,可以选定一项。

  为什么将一个库用于单线程应用程序,而将另一个库用于多线程应用程序?究其原因,还是因为标准C运行时库问世的比较早,远远早于线程在应用程序的应用。标准C运行时库的发明者并没有考虑过多线程的问题。假定有下面的代码:

 BOOL fFailure = (system("NOTEPAD.EXE README.TXT")==-);
if(fFailure){
switch(error){
case E2BIG: // Argument list of environment to big
break;
case ENOENT: // Command interpreter cannot be found
break;
case ENOEXEC: // Command interpreter has bad format
break;
case ENOMEM: // Insufficient memory to run command
break;
}
}

  假设某个线程在第一行执行后和第二行执行前,线程中断了运行,又假设中断运行是为了让同一个进程中的另一个线程开始执行,而这个新线程将执行另一个负责设置全局变量errno的/C运行时函数。这样当CPU重新分配给第一个线程的时候,errno的值将不在能够反映上面代码中出错的错误代码。

  为了解决这个问题,就要每个线程都有自己的errno,而不影响其他线程的errno,以及_doserrno,strtok,_wcstok,strerror,_strerror,tmpnam,tmpfile,asctime,_wasctime,gmtime,_ecvt,fcvt等。

  可是系统并不知道你的程序使用CC++编写的,更不知道你调用函数的线程本就是不安全的。问题在于你必须正确地进行所有的操作。若要创建一个新线程,绝对不要使用操作系统的CreateThread,必须使用CC++运行时库函数__beginthreadex:

 unsigned long _begintheradex(
void *security,
unsigned stack_size,
unsigned (*start_addres)(void *),
void *arglist,
unsigned initflag,
unsigned *thrdaddr);

  注意,这个函数只存在与CC++运行时库的多线程版本中,如果链接到单线程的版本,链接就会得到一个“未转换的外部符号”错误消息。当然,从设计上讲,单线程库是不能在多线程应用程序中正确运行的。另外要注意,VisualStudio在创建新项目时默认使用的是单线程库。这并不是最安全的默认设置,对于多线程应用程序来说,应该显示地转换到多线程的版本。

  由于Microsoft为CC++运行时库提供了源代码,因此很容易确定CreateThread_beginthreadex两者之间的差别。_beginthreadex的源代码在threadex.c中。  

C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\crt\src\threadex.c

  关于_beginthreadex有下面这些:

  • 每个线程都获得由CC++运行时库的堆栈分配的自己的tiddata内存结构(在mtdll.h文件中)。
  • 传递给_beginthreadex的线程函数保存在tiddata中,传递给这个函数的参数也保存在这个结构中。
  • _beginthreadex的实现来看确实是在内部调用了CreateThread,这是操作系统唯一知道的创建新线程的方法。
  • 当调用CreateThread时,它被告知通过调用_threadstartex而不是pfnStartAddr来执行新线程。
  • 传递给线程函数的参数是tiddata结构而不是pvParam的地址。
  • 如果一切顺利,返回线程句柄,否则返回NULL。

  这个结构和线程关联的方式是通过函数_threadstartex函数(和_beginthreadex在同样的文件中)。关于_threadstartex

  • tiddata的地址作为唯一的参数被传递给_threadstartex
  • TlsSetValue是一个操作系统函数,负责将一个值与调用线程关联起来。Tls的术语叫做线程本地存储
  • 一个SEH帧被放置在线程函数的周围,负责处理与运行时库的许多事情:
    • 运行时错误
    • CC++运行时库的signal函数。如果线程是用CreateThread创建的,又调用signal函数,那么函数就不能正确的运行。
  • 调用必要的线程函数,传递必要的参数。记住,函数和参数的地址由_beginthreadex保存在tiddata中。
  • 线程函数的返回值被认为是线程的退出代码。注意,_threadstaartex不是返回到BaseThreadStart。如果它这样做,线程就终止运行,退出代码被正确地设置,线程的tiddata块却不会被撤销。这将导致一个漏洞。若要防止这个漏洞,就要调用_endtheadex(同样位于Threadex.c文件)。

  关于_endtheadex

  • _getptd函数内部调用TlsGetValue函数,它负责检索调用线程的tiddata块的地址。
  • 该数据块被释放,操作系统调用EndThread以便真正地撤销线程。

Microsoft的VisualC++开发小组认识到编程人员喜欢调用ExitThread,就实现了他们的愿望,还不会让应用程序占用内存。如果真的想强制撤销线程,可以调用_endthreaded(而不是ExitThread)以便释放tiddata块,然后退出。

  现在数据块和线程关联起来,每个线程都有各自的内存存放数据了。例如errno在多线程环境下实际上是一个宏了,

  显然,上面说到的那些操作都会影响多线程版本的CC++运行时库的性能,这也是为什么吗Microsoft公司除了多线程版本,还提供单线程版本的静态链接库的原因。

  CC++运行时库的动态链接版本编写成一种通用的版本,这样就可以被使用CC++运行时库函数的所有正在运用的应用程序和DLL共享。由于这个原因,运行时库质询在与多线程版本中。由于DLL中提供了CC++运行时库,exe和dll就包含更少的代码,编译出来的规模更小。同时如果Microsoft排除了CC++运行时库DLL中的错误,应用程序中的错误也会自动得到解决。


不应该使用的CC++运行时库函数

  另外两个函数:

unsigned long _beginthread(
void (__cdecl *start_address)(void *),
unsigned stack_size,
void *arglist);
void _endthread(void);

  的确它们和_beginthreadex以及_endtheadex有相似的功能,但是参数更少,不如后者全面。使用_beginthread的话,就不能创建带有安全属性的线程、无法创建可以暂停的线程和无法获得线程的ID值。函数_endthead不仅如此,还有更大的问题,就是_beginthread创建的线程终止后,调用CloseHandle即将失败。


自己的ID的概念了解一下

  为了便于操作进程和进程中的线程,Windows提供了一些函数使得线程很容易引用它的进程内核对象和线程内核对象:

HANDLE GetCurrentProcess(); // 进程的伪句柄
HANDLE GetCurrentThread(); // 线程的伪句柄

  这两个函数并没有在句柄表中创建新句柄,也没有影响到引用计数,即使调用CloseHandle把伪句柄当作参数,CloseHandle也会忽略并返回FALSE。

  如果想要实句柄,就要先通过下面的函数拿到进程或者线程的ID:

DWORD GetCurrentProcessId();
DWORD GetCurrentThreadId();

  在调用:

HANDLE OpenProcess(
DWORD dwDesiredAccess, //渴望得到的访问权限(标志)
BOOL bInheritHandle, // 是否继承句柄
DWORD dwProcessId// 进程标示符
);
HANDLE WINAPI OpenThread(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ DWORD dwThreadId
); 

  当然,如果只有一个伪句柄,又想获得实句柄,可以使用DuplicateHandle函数转换。

BOOL DuplicateHandle(
HANDLE hSourceProcess,
HANDLE hSource,
HANDLE hTargetProcess,
PHANDLE phTarget,
DWORD fdwAccess,
BOOL bInhberitHandle,
DWORD fdwOption);

  书中的例子:

DWORD WINAPI ParentThread(PVOID pvParam){
HANDLE hThreadParent;
DuplicateHandle(
GetCurrentProcess(),
GetCurrentThread(),
GetCurrentProcess(),
&hThreadParent,
,
FALSE,
DUPLICATE_SAME_ACCESS); //递增引用计数
CreateThread(NULL,,ChildThread,(PVOID)hThreadParent,,NULL);
}
DWORD WINAPI ChildThread(PVOID pvParam){
HANDLE hThreadParent = (HANDLE)pvParam;
FILETIME ftCreationTime,ftExitTime,ftKernelTime,ftUserTime; GetThreadTimes(hThreadParent,&ftCreationTime,&ftExitTime,&ftKernelTime,&ftUserTime);
CloseHandle(hThreadParent); //递减引用计数
}

  这样就可以用过hThreadParent这个变量影响主线程了。

  注意DuplicateHandle会递增转换后的实句柄的引用计数,因此必须用CloseHandle递减。

回炉重造之重读Windows核心编程-006-线程的更多相关文章

  1. 回炉重造之重读Windows核心编程-003-内核对象

    内核对象是个比较难理解的概念,问题的根源就在于即使是<核心编程>书中也没有说清楚它的定义,只是不停地举例和描述它的性质,还有如何使用. 盲人摸象,难见全貌.只能尽可能列举它的性质,注意使用 ...

  2. 回炉重造之重读Windows核心编程-002-字符集

    使用Unicode的优势: 便于在不同语言之间进行数据交换. 让你的exe或者dll文件支持所有的语言. 提高应用程序的执行效率. Windows2000是使用Unicode重新开发的,核心部分都需要 ...

  3. 回炉重造之重读Windows核心编程-004-进程

    进程是一个正在运行的程序的实例,由内核对象和地址空间组成.进程是不活泼的,执行地址空间中代码的是在它的环境中运行线程.每个线程都需要自己的一组CPU寄存器和堆栈. 为了让所有线程都能运行,操作系统就要 ...

  4. 回炉重造之重读Windows核心编程-001-错误处理

    Windows处理错误靠的是API的返回值,类型不止一种种: VOID,函数不可能失败,Windows API的返回值很少是这个情况. BOOL,如果函数失败,则返回值是0,否则返回是非零值.不要测试 ...

  5. 【windows核心编程】线程局部存储TLS

    线程局部存储TLS, Thread Local Storage TLS是C/C++运行库的一部分,而非操作系统的一部分. 分为动态TSL 和 静态TLS 一.动态TLS 应用程序通过调用一组4个函数来 ...

  6. 《windows核心编程》- 线程栈

    当系统创建线程的时候,会为线程栈预订一块地址空间区域,并给该区域调拨一些物理存储器.默认会预订1MB的地址空间并调拨两个页面的存储器.但是在构建 应用程序的时候可以改变这个默认值 在构建应用程序的时候 ...

  7. 《windows核心编程系列》二十一谈谈基址重定位和模块绑定

    每个DLL和可执行文件都有一个首选基地址.它表示该模块被映射到进程地址空间时最佳的内存地址.在构建可执行文件时,默认情况下链接器会将它的首选基地址设为0x400000.对于DLL来说,链接器会将它的首 ...

  8. Asp.Net SignalR 使用记录 技术回炉重造-总纲 动态类型dynamic转换为特定类型T的方案 通过对象方法获取委托_C#反射获取委托_ .net core入门-跨域访问配置

    Asp.Net SignalR 使用记录   工作上遇到一个推送消息的功能的实现.本着面向百度编程的思想.网上百度了一大堆.主要的实现方式是原生的WebSocket,和SignalR,再次写一个关于A ...

  9. windows核心编程 - 线程同步机制

    线程同步机制 常用的线程同步机制有很多种,主要分为用户模式和内核对象两类:其中 用户模式包括:原子操作.关键代码段 内核对象包括:时间内核对象(Event).等待定时器内核对象(WaitableTim ...

随机推荐

  1. 响应国家号召,在家撸码之React迁移记

    最近这段时间新型冠状病毒肆虐,上海确诊人数每天都在增加,人人提心吊胆,街上都没人了.为了响应国家号召,近期呆在家里撸码,着手将项目迁移到React中,项目比较朴素,是一张线索提交页面,包含表单.图片滚 ...

  2. Kotlin DSL for HTML实例解析

    Kotlin DSL for HTML实例解析 Kotlin DSL, 指用Kotlin写的Domain Specific Language. 本文通过解析官方的Kotlin DSL写html的例子, ...

  3. centos7.6+python3+apache2.4+django2.1.2网站部署总结

    本次网站部署是使用了django2.1.2版本部署,由于centos自带的Python2.7不支持django2.0以上版本,故需要安全python3的环境.python3.apache的安装不做具体 ...

  4. 项目SpringMVC+Spring+Mybatis 整合环境搭建(2)-> 测试Spring+Mybatis 环境

    测试前期准备 第一步:创建easybuy数据库,设置utf-8格式 第二步:创建表test_tb CREATE TABLE `test_tb` ( `id` int(11) NOT NULL AUTO ...

  5. 实验2: CDP命令操作

    基本命令 1.获设备的相邻信息:CDP CDP1.不管上层协议 能够获取的信息包括:1.设备名2.对应各协议的地址3.端口名4.角色特征5.平台 全局启用/关闭CDP:cdp run/no cdp r ...

  6. 中国天气网API接口

    http://www.weather.com.cn/data/sk/101010100.html http://www.weather.com.cn/data/cityinfo/101010100.h ...

  7. 《Python学习手册 第五版》 -第13章 while循环和for循环

    上一章已经讲过if条件语句,这章重点是循环语句:while.for 本章的重点内容 1.while循环 1)一般形式 2)break.continue.pass和循环的else 2.for循环 1)一 ...

  8. input . type=number.使用后问题点

    所有主浏览器都支持type属性,但是,并非所有主流浏览器都支持所有不同的 input 类型. 以下 input 类型是 HTML5 中的新类型:color.date.datetime.datetime ...

  9. Go语言实现:【剑指offer】数字在排序数组中出现的次数

    该题目来源于牛客网<剑指offer>专题. 统计一个数字在排序数组中出现的次数. 看到排序数组,要想到用二分查找. 先找到最前面的数字k,再找到最后面的数字k,通过下标求出次数. Go语言 ...

  10. C++中类成员变量在初始化列表中的初始化顺序

    引子:我们知道,C++中类成员变量的初始化顺序与其在类中的声明顺序是有关的. 先看代码: class TestClass1 { public: TestClass1() { cout << ...