win32 多线程

来源:互联网 发布:实况足球2017mac版 编辑:程序博客网 时间:2024/05/16 15:55

线程是不能但对存在的,其必须存在在进程的地址空间中。一个线程在这段地址空间仅有两样东西

  • 一个线程的内核对象,操作系统使用这个数据结构来管理线程。
  • 一个线程栈,其中存储着所需函数的参数和局部变量

创建线程

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
终止别线程或者自己。注意三点

  1. 此函数不能保证立马终止线程(它只确保消息的发出)。还需要别的手段来确定线程是否终止如(如WaitForSingleObject)
  2. 函数返回或者调用ExitThread 会销毁线程栈。但是TerminateThread方式“杀死”的线程,除非进程终止,不然不会系统不会销毁进程段。windows 这样设计为了让线程被终止之后别的线程不会访问违规(如,运行的线程还引用者被杀死线程的数据)
  3. 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