第六章:线程基础

来源:互联网 发布:js 怎么实现邮箱注册 编辑:程序博客网 时间:2024/06/06 03:27

第六章1.线程的组成:
● 一个是线程的内核对象,操作系统用它管理线程.系统还用内核对象来存放线程统计信息的地方.
● 一个线程栈,用于维护线程执行时所需的所有函数参数和局部变量

2. 线程的入口函数:
DWORD WINAPI ThreadFunc( PVOID pvParam)
在使用线程时需要注意

■ 在默认情况下,主线程的入口函数必须命名为main,wmain,WinMain或者wWinMain(除非我们使用了/ENTRY:链接器选项来指定另一个函数作为其入口函数).于此不同的是,线程函数时可以任意命名的.事实上,如果应用程序中有多个线程函数,必须为他们指定不同的名称,否则编译器/链接器会认为你创建了一个函数的多个实现

■ 因为主线程的入口点函数有字符串参数,所以提供了ANSI/Unicode版本供我们选择:main/wmain和WinMain和wWinMain.相反,我们不必担心ANSI/Unicode问题

■ 线程函数必须返回一个值,他会成为线程的退出码.这类似C++/C运行库策略:令主线程的退出码成为进程的退出码.

■ 线程函数(实际包括所有函数)应该尽可能的使用函数参数和局部变量.使用静态变量和全局变量时,多个线程都可访问这些变量,这样可能破坏变量中保存的内容.其实如果从资源的利用来讲,静态变量和全局变量是在整个对象存在时一直占据内存的.

调用线程创建函数(CreateThread),系统会创建一个线程内核对象.这个线程内核对象不是线程本身,而是一个较小的数据结构,操作系统利用这个结构来管理线程.可以把内核对象想象为一个由线程统计信息构成的小型数据结构.这与进程和进程内核对象之间的关系是相同的.另外我们在写代码的时候尽量使用_beginthreadex来代替CreateThread(或者使用编译器提供商的类似函数来替代他).

3.线程创建函数:

HANDLE CreateThread(

PSECURITY_ATTRIBUTES psa,//安全属性

DWORD chStackSize, //堆栈大小

PTHREAD_START_ROUTINE pfnStartAddr, //线程执行函数的起始地址

PVOID pvParam, //传递给起始地址的参数

DWORD dwCreateFlags, //创建标志

PDWORD pdwThreadID) //线程ID

参数:

◆ psa:如果想要使用默认的安全属性,则传递NULL.如果希望所有子进程都能继承到这个线程对象的句柄,必须指定一个SECURITY_ATTRIBUTE结构,并将该结构的bInheritHandle成员初始化为TRUE.

◆ cbStackSize:指定线程可以为其线程栈使用多少地址空间.每个线程都有自己的栈.对于该参数,CreateProcess使用了保存在可执行文件内部的一个值.可以在编译器中设置.如果传入非0值,函数会为线程栈预定空间并为之调拨所需的所有存储空间;如果传入0值,函数就会预定一个区域,并根据由/STACK链接器开关指定的存储量来调拨储存器

◆ pfnStartAddr和pvParam:希望新线程执行的线程函数的地址和参数.pvParam可以是一个数值,也可以是一个指向一个数据结构(其中包括额外信息,例如this)的指针.

◆ dwCreateFlags:如果为0,,则创建之后立即就可进行调度.如果只为CREATRE_SUSPENDED,系统将创建并初始化线程,但是会暂停该线程的运行,这样他就无法进行调度.

◆ pdwThreadID:用来储存系统分配给新线程的ID

4.终止线程
■ 线程可以通过一下四种方式来终止运行
● 线程函数返回(这个强烈推荐的)
● 线程通过调用ExitThread函数“杀死”自己。
● 同一个进程或者另一个进程中的线程调用TerminateThread函数
● 包含线程的进程终止运行
■ 线程函数的返回:可以确保一下正确的应用程序清理工作都得以执行 
● 线程函数中创建的所有C++对象都是通过其析构被正确销毁
● 操作系统正确释放线程栈使用的内存
● 操作系统把线程的退出码设为线程函数的返回值
● 系统递减线程的内核对象计数
■ ExitThread函数:
VOID ExitThread(DWORD dwExitCode);
该函数将终止线程的运行,并导致操作系统清理该线程的所有系统资源.但是你的C/C++资源(对象等)不会被销毁
也可以使用ExitThread的dwExitCode参数来告诉系统将线程的退出码设为什么.函数本身没有返回值(因为线程已经终止了).
注意:结束线程函数要配套使用:CrateThread对应ExitThread;_beginthreadex对应_endthread.)
■ TerminateThread函数:
BOOL TerminateThread(
HANDLE hThread,
DWORD  dwExitCode)
不同于ExitThread杀死主调线程,TerminateThread能杀死任何线程.
注意:TerminateThread函数时异步的.即调用可以能会存在"延时",如果想要确认线程已经终止,可以调用WaitForSingleObject或类似的函数,并向其传递线程的句柄.
■ ExitThread和TerminateThread的区别:
  如果通过返回或调用ExitThread函数的方式来终止一个线程的运行,该线程的堆栈也会被销毁(只是对象不会).但如果调用的是TerminateThread,除非拥有此线程的进程终止运行,否则系统不会销毁这个线程的堆栈.此外,如果使用ExitThread,则DLL会再线程终止时受到通知,而TerminateThread不会.

■ 对于ExitProcess和TerminateProcess也可用于终止线程的运行.区别在于:这些函数会使终止运行的进程中的所有线程全部终止.(类似我们为剩余的每个线程都调用了TerminateThread).意味着:C++对象的析构函数不会被调用.数据不会回写到磁盘.
■ 线程终止运行时,会发生以下事情:
● 线程拥有的所有用户对象句柄会释放.在windows中,大多数对象都是包含了"创建这些对象的线程"的进程拥有的.但一个线程有两个用户对象:窗口和挂钩.一个进程终止运行时,系统会自动销毁有线程创建或安装的任何窗口.并卸载由线程创建或安装的任何挂钩.其他对象只有在拥有线程的进程终止时才被销毁
● 线程的退出码从STILL_ACTIVE变成传给ExitThread和TerminateThread代码

● 线程内核对象的状态变成触发状态

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

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

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

■ 可以使用GetExitCodeThread来检查hThread所标示的线程是否已经终止了.如果是,那可判断其退出码是什么:

BOOL GetExitCodeThread(HANDLE hThread,

PDWORD pdwExitCode)//线程尚未终止则设为STILL_ACTIVE.

如果调用成功,则返回TRUE.

5.线程创建和初始化图:


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

一旦创建了内核对象,系统就分配内存,供线程的堆栈使用.然后系统将两个值写入新线程堆栈的最上端(线程堆栈始终是从高位内存地址想地位内存地址构建的).写入线程堆栈的第一个值是传给CreateThread函数的pvParam参数,紧接着在它的下方是传给CreateThread函数的pfnStartAddr值.

每个线程都有其自己的一组CPU寄存器(即线程的上下文).他反映了当线程上一次执行时线程的CPU寄存器的状态.线程的CPU寄存器全部保存在一个CONTEXT结构(它本身保存在线程的内核对象中).

指令指针寄存器(IP)和栈指针寄存器(SP)是线程上下文中最重要的两个寄存器.这个两个地址标示的内存都位于线程所在进程的地址空间中.当线程的内核对象被初始化的时候,CONTEXT结构的堆栈指针寄存器(SP)被设置为pfnStartAddr在线程对战的地址.而指令指针寄存器被设置为RtlUserThreadStart函数的地址(该函数是NTDLL.dll模块导出的.

其基本操作如下:

Void RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr,PVOID pvParam)

//这两个参数是系统显式的写入线程堆栈.(有些则是使用CPU寄存器来传递的)

{

__try{

ExitThread((pfnStartAddr)(pvParam));

}

__except(UnhandleExceptionFilter(GetExceptionInformation())){

ExitProcess(GetExceptionCode());

}

}

线程完全初始化好之后,系统将检查CREATE_SUSPENDED标志是否已被传给CreateThread函数.如果此标志没有被传递,系统将线程的挂起计数递减至0;随后线程就可以调度给一个处理器去执行.然后系统在实际的CPU寄存器中加载上一次在线程上下文中保存的值.

因为新线程的指令指针被设置为RtlUserThreadStart,所以这个函数实际就是线程开始执行的地方.

新线程执行RtlUserThreadStart函数将一下事情:

● 围绕线程函数,会设置一个结构化异常处理(SEH)帧.这样如果线程出现异常都能得到系统的默认处理.

● 系统调用线程函数,把传给CreateThread函数的pvParam参数传给他.

● 线程函数返回时,调用ExitThread,将你的线程函数的返回值传给他.递减线程内核对象计数,而后线程停止.

● 如果线程产生了一个未被处理的异常.函数的SEH帧会处理这个异常.

注意:在RtlUserThreadStart内,线程会调用ExitThread或者ExitProcess.这意味着线程永远不能退出此函数;他始终在其内部消亡

一个进程的主线程初始化时,其指令指针会被设为同一个未文档化的函数RtlUserThreadStart.当该函数开始运行时,他会调用C/C++运行库的启动代码,后者初始化继而调用你的_tmain或_tWinMain函数,你的入口点函数返回时,C++/C运行时启动代码会调用ExitProcess.因此他不会回到RtlUserThreadStart函数.

6.为了保证C/C++多线程应用程序正常运行,必须创建一个数据结构,并使之与使用了C/C++运行库函数的每个线程关联.然后调用C/C++运行库函数时,那些函数必须知道如何去查找主调线程的函数块,从而避免影响到其他线程.
为此,为了要使得线程安全,一定不要调用CreateThread,相反必须调用C/c++运行库函数_beginthreadex:
unsigned long _beginthreadex(
void *security,
unsigned stack_size,
unsigned (*start_address)(void*),
void arglist,
unsigned initflag,
unsigned *threadaddr);
在使用时,要进行强制转化.
_beginthreadex所作的操作:

1.  _beginthreadex(...)

2.  {

3.  _ptiddata ptd;

4.  if( ( ptd = ( _ptiddata )_calloc_crt( 1,sizeof(struct _tiddata) ) ) == NULL)//分配内存(从C/C++运行库的heap上分配的

5.  //这个结构包含了pvParam和pfnStartAddr

6.  {

7.  //其他操作

8.  }

9. 

10.  //初始化对象

11.  ptd->_initaddr = ( void* )pfnStartAddr;

12.  ptd->_initarg = pvParam;

13.  ptd->_thandle = ( uintptr_t )( -1 );

14. 

15.  uintptr_t thdl = ( uintptr_t )CreateThread( ( LPSECURITY_ATTRIBUTES )psa,

16.  cbStackSize,

17.  _threadstartex,

18.  ( PVOID )ptd,dwCrateFlags,

19.  pdwThreadID );

20.  //这里面其实牵涉到如何使得ptd结构和线程关联问题

21.  }

22. 

23.  _threadstartex( void* ptd )

24.  {

25.  TlsSetValue( _tlsindex,ptd );//这个函数可以理解成将变量设置进进程内

26.  //如果想要获取出来,可以使用 TlsGetValue()

27.  _callthreadstartex();

28.  }

29. 

30.  _callthreadstartex()

31.  {

32.  ptd = getptd();//获取ptd

33. 

34.  __try{

35.  _endthreadex( ( ( unsigned (WINAPI *) ( void * ))(((ptiddata)ptd)->_initaddr) )(((_ptiddata)ptd)->_initarg) );

36.  }

37.  __except( _XceptFileter( GetExceptionCode(),GetExceptionInformation() ) )

38.  {

39.  _exit(GetExceptionCode() );

40.  }

41.  }

42. 

43.  _endthread( unsigned retcode )

44.  {

45.  ptd = _getptd_noexit();

46. 

47.  if ( ptd != NULL )

48.  {

49.  _freeptd();//释放内存

50.  }

51. 

52.  ExitProcess( retcode );

53.  }

就像我们所期望的那样,C/C++运行库的启动代码为应用程序的主线程分配并初始化了一个数据块.这样一来,主线程就可以安全的调用任何C/C++运行库函数(同理在返回的时候会释放数据块).此外,启动代码设置了正确的结构化异常处理代码,使主线程能成功调用C/C++运行库的signal函数.

总结:用_beginthreadex而不要使用CreateThread创建线程.而且不应该是_beginthread和_endthread(相对于_endthreadex它会额外的关闭句柄)两个函数.

7. 几个常用的函数:
HANDLE GetCurrentProcess();
HANDLE GetCurrentThread();
这两个函数返回的是主调线程的进程内核对象和线程内核对象的伪句柄(pseudohandle).即指向当前线程的句柄(谁发出函数调用,就指向谁).

他不会在主调进程的句柄表中新建句柄.而且不会影响到进程内核对象或线程内核对象的使用计数.(如果调用CloseHanle,将一个伪句柄传入,该操作将忽略,并返回FALSE,同时GetLastError将返回ERROR_INVALID_HANDLE ).
相对应的两个函数:

DWORD GetCurrentProcessId()

DWORD GetCurrentThreadId()

如果想将伪句柄转化为真正的句柄可以调用DuplicateHandle

BOOL DuplicateHandle(

HANDLE hSourceProcessHandle,  // handle to source process

HANDLE hSourceHandle,         // handle to duplicate

HANDLE hTargetProcessHandle,  // handle to target process

LPHANDLE lpTargetHandle,      // duplicate handle

DWORD dwDesiredAccess,        // requested access

BOOL bInheritHandle,          // handle inheritance option

DWORD dwOptions)               // optional actions

参数:

hSourceProcessHandle:源进程内核句柄(即负责传递内核对象句柄的进程句柄)

hSourceHandle:要传递的内核对象句柄(线程)

hTargetProcessHandle:目标进程内核句柄

lpTargetHandle:接收内核对象句柄的地址(先随便声明一个HANDLE)

dwDesiredAccess:TargetHandle句柄用何种访问掩码(这个掩码是在句柄表中的一项)

bInheritHandle:是否拥有继承

dwOptions:当设DUPLICATE_SAME_ACCESS时,表示于源的内核对象所有标志一样,此时wDesiredAccess可标志为0.

需要注意的是,转换完成以后,内核使用计数已经+1,应该立即调用CloseHandle递减.