线程是不能但对存在的,其必须存在在进程的地址空间中。一个线程在这段地址空间仅有两样东西
- 一个线程的内核对象,操作系统使用这个数据结构来管理线程。
- 一个线程栈,其中存储着所需函数的参数和局部变量
创建线程
HANDLE WINAPI CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
dwStackSize
设置线程栈预留空间大小或者是调拨大小(见dwCreationFlags参数)。默认情况情况是1MB地址空间并调拨两个页面。或者我们可以使用编译器的/F选项和连接器/STACK选项把数据信息写入exe和dlll的PE头文件中。
/STACK:reserve[,commit]
如果该参数为0.系统会使用PE头文件中的指定的大小。
Tips:
我们为线程创建线程栈大小有一个好处,就是可以提前发现函数无限递归。
lpStartAddress
执行线程任务的函数
typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(
LPVOID lpThreadParameter
);
lpParameter
这个参数是会传递lpStartAddressha函数
dwCreationFlags
就三个值。
如果希望线程创建晚立刻运行可以设置为0
如果希望线程先suspend(挂器),也就是不运行可以设置CATESUSPENDED。通过ResumeThread来运行
最后一个开关将会指定dwStackSize表示的是堆栈大小还是堆栈提交大小
lpThreadId
指定线程id。一般传入nullptr,让操作系统为我们陪陪PID
终止线程
终止线程一个线程有以下方法
- 线程函数返回,强烈建议
- ExitThread,避免使用
- TerminateTread,避免使用
- 包含线程的进程终止,避免使用
线程返回
一个线程如果返回时。可以确保一下清理工作的进行
- 线程函数中的c++对象的析构函数会被调用
- 操作系统释放线程栈的内存(什么方式都是,除了TerminateThread)
- 操作系统把线程返回值设置为线程退出代码
- 系统减少内核对象的使用计数
ExitThread和TerminateTread函数对应者ExitProcess和TerminateProcess。它们都会让线程和进程戛然而止而得不到应有的c运行时清理。写c/c++代码时,强烈建议不要使用这些函数。而是应该设计好函数的流程使其自然返回。
ExitThread
【无】
TerminateThread
终止别线程或者自己。注意三点
- 此函数不能保证立马终止线程(它只确保消息的发出)。还需要别的手段来确定线程是否终止如(如WaitForSingleObject)
- 函数返回或者调用ExitThread 会销毁线程栈。但是TerminateThread方式“杀死”的线程,除非进程终止,不然不会系统不会销毁进程段。windows 这样设计为了让线程被终止之后别的线程不会访问违规(如,运行的线程还引用者被杀死线程的数据)
- dll通常会在线程终止的时候的得到通知,而TerminateThread终止时不会受到消息,所以不会正常完成清理工作
线程终止
线程终止会发生一下事情
- 线程的所有对象句柄会被释放(就像所有句柄调用CloseHandle一样 )。
- 线程的退出代码由STILL_ACTIVE变成返回代码
- 线程的内核对象变成触发状态
- 如果线程是最后一个活动线程,那么进程也会终止
- 线程的内核对象使用计数-1
GetExitCodeThread来获得返回代码
线程原理的简单介绍
上面的图简单的概括了线程的原理。在线程内核中有一些统计信息之外有一个很重要的结构Context(winnt.h)。它是线程自己的一组cpu寄存器。这个结构中又两个非重要的寄存器,堆栈指针寄存器指向线程栈。指令指针寄存器指向这个Ntdll,dll中的RtlUserThreadStart函数。而我们的线程运行就是存这个函数开始,它做一下事情
- 围绕线程函数,设置一个机构化异常处理(struct exception handling ,SEH)。这样线程执行期间产生的任何异常都可以得系统的默认处理
- 通过lpStartAddress调用线程函数,把lpParameter参数传递给他
- 线程返回时,调用ExitThread,并把返回值传给他。
- 如果线程产生一个未处理异常,RtlUserThreadStart设置的SEH会处理这个异常。这样通常会为用户显示一个对话框。而且用户如果关对话框。RtlUserThreadStart会调用ExitProcess来关闭整个进程而不是线程
我们要注意一下几点
1. RtlUserThreadStart会调用ExitProcess或者ExitThread来终止线程,所以这个函数永远不会返回
2. 线程是真正的在这个函数开始运行的。之所以能访问该函数的参数。是因为操作系统帮我们帮该函数的参数压栈进线程栈了(lpParameter和lpStartAddress)。但是该线程不会被返回也不能被返回。因为线程栈中没有该函数的返回地址。如果返回会出现访问违规。因为没有返回地址它只能返返回到任意地方
3. 如果是主线程那么他的指令寄存器会指向另一个也叫做RtlUserThreadStart的函数。但是他会调用C/C++的启动函数
c/c++在windows中的多线程情况
因为c标准的运行库是在1970左右被发明的。而那时候是没有多线程这个概念的。所以c的运行库不是多线程安全的。
一标准的C全局变量errno为例:
BOOL b = (system("NOTEPAD.exe, READEME.txt") == -1);
if (b) {
switch (errno) {
case E2BIG :// argumnet list or envrionment too big;
break;
case ENOENT:// command interpreter not be fount;
break;
case ENOEXEC:// command interpreter has bad format
break;
case ENOMEM:// insuffcient memory to run command
break;
}
}
如果上面运行if 的时候cpu 突然调到别的线程运行,而别的线程也产生了一个errno 。先cpu 又回到了次线程来运行。可是这个时候线程的errno的值已经变了。相似的例子还有很多
所以标准的c/c++运行库不是为多线程设计的。但是问题还是要解决的。怎么才能在不重新设计c/c++运行库的情况让他们线程安全呢。
所以Microsoft的程序员写了一个新函数_beginthreadex
_ACRTIMP uintptr_t __cdecl _beginthreadex(
_In_opt_ void* _Security,
_In_ unsigned _StackSize,
_In_ _beginthreadex_proc_type _StartAddress,
_In_opt_ void* _ArgList,
_In_ unsigned _InitFlag,
_Out_opt_ unsigned* _ThrdAddr
);
该函数在process.h头文件中定义。可以看见函数以下划线开头。所以是windows 特有的。看见他的参数和CreateThread一样但是类型好像有点区别。因为写这个函数的程序员认为既然是C/C++运行库的函数,当然不应该有WINDOWS的数据类类型。
_beginthreadex这个函数的源代码在Threadex.c中。
它主要做一下几件事
- 在堆上面创建_tiddata数据块(源代码在Mtdll.h)并且将
_ArgList(函数参数地址)和_StartAddress(任务函数地址)参数保存到_tiddata中,还要有c++中可能产生多线程问题的全局变量放入其中。 - 调用CreateThread。lpStartAddress参数为_threadTreadex函数地址。lpParameter参数为_tiddata数据块地址
- 如果一切顺利返回线程句柄,否则返回返回0
如果创建线程成功就来都了RtlUserThreadStart.这个函数设置玩SEH之后就调用_threadTreadex函数了,并传给它_tiddata
_threadTreadex它主要做一下事情
- 调用TlsSetValue将一个值与主调线程关联起来。这就是所谓的线程局部存储(Thread local storage,TLS)。使_tiddata和主调线程关联起来
- 之后调用_callthreadstartex。
- 该函数不会被返回。后面你就知道了
_callthreadstartex完成一下事情
- 这个函数有一个SEH.帧。通过这个帧处理与运行库有关的许多事情。运行时错误就是它的一个任务。还有一个非常重要的就是signal函数。不然signal就无法正常使用
- 调用任务函数(我们写的线程函数),并传递参数给他。前面说过它和其参数被_begainThreadex存在_tiddata中。
- 调用玩我写的函数之后调用_endthreadex函数,并把返回值给它
_endthreadex函数做一下事情
- TlsGetValue和的 _tiddata内存块,并释放
- 调用ExitThead结束线程,并用_endthreadex的参数(我们函数的返回值)设置退出代码
通过上面的程序之后。我们的每一个线程都有一个内存区域来存放变量了。之后的c/c++运行库中有多线程问题的函数就可以通过访问访问线程惯量的_tiddata块就可以解决多线程的问题了(通过TlsGetValue)。但是全局变量怎么办。它们可是进程中所有线程共享的。
但是通过源代码我们可以知道
_ACRTIMP int* __cdecl _errno(void);
#define errno (*_errno())
全局变量只不过是一个函数的宏。而这个函数在c/c++内部又是同上面的方法来访问给子的全局变量的。在以前如果希望运行多线程版C/C++运行库需要链接多线版本的运行库但是现在
c/c++运行运行库不在区分单线程版还是多线程版了,它们都是多线的
_begainThreadex和CreateThread
通常情况大部分的c/c++运行库函数都是线程安全的。所有
_begainThreadex和CreateThread使用哪一个都是一样的。但是存在线程安全的函数会怎么样呢?和原先的相比它们被重新设计一遍访问_tiddata数据块。它们会首先检查是否有关联的_tiddata(通过TlsGetValue)。如果没有的话就会创建一个_tiddata.然后在关联这个_tiddata(通过TlsSetValue)。而且来链接到C/C++运行库的dll版本时。当线程终止时。会受到一个DLL_THREAD_DEATH的通知,从而释放_tiddata块。
以上的所有好处好像在说明_begainThreadex和CreateThread都差不多。但是别忘了_begainThreadex除了设置_tiddata块还有很多工作要做(异常处理,siganl函数)。所以建议在书写c/c++代码时使用_begainThreadex函数
创建线程的三个方法
http://www.cnblogs.com/TenosDoIt/archive/2013/04/15/3022036.html
线程问题解决
http://blog.csdn.net/morewindows/article/details/7392749