浅墨浓香

想要天亮进城,就得天黑赶路。

导航

第6章 线程基础

Posted on 2015-08-03 00:19  浅墨浓香  阅读(666)  评论(0编辑  收藏  举报

6.1 线程基础

(1)线程组成:线程内核对象+线程栈(★进程=进程内核对象+地址空间

  ①从内核角度看,线程是一个内核对象,系统用它来存储一些关于线程的统计信息(比如运行时间等)

  ②从编程角度看,线程是一堆寄存器状态以及线程栈的一个结构体对象。本质上可以理解为一个函数的调用器(其中的寄存器状态用于控制CPU执行,栈用于存储局部变量和函数参数及函数的返回地址)——为什么要使用线程栈的?

线程1

线程2

备注(使用线程栈的原因分析)

void func1(){

  int a;

  int b;

}

void func2(){

  int c;

  int d;

}

如果不为每个线程分配线程栈,而使用进程中某一共同的栈,设func3先于func4执行,则变量进栈顺序a、b,如果此时执行线程2,则c、d也会进栈,栈顶指针指向d。假设这时func3执行完,要回收栈则会出现将c、d弹出栈的错误。现实中可能会出现更复杂的情况。当然,如果这两个线程严格串行执行,则不会出现这种错误。

  ③线程还可以带有消息队列(GUI线程内部会创建)和APC队列。(但注意这些队列在线程创建时并不同时创建,要在调用GUI函数里才会被创建!)

★进程是线程的容器,线程共享进程的地址空间和资源

(2)什么时候不使用多线程

  ①当一个算法本身是严格串行化的时候,即计算的每一步都严重依赖前一个操作步骤的结果时,不适合用多线程)。

  ②当多个功能任务具有比较严格的先后逻辑关系时,不宜采用多线程。因为这涉及到线程同步方法的严格控制,从而可能因加了过多的同步而降低了效率。

  ③还有一种特殊情况,比如一个服务器需要处理成千上万个客户端连接,不宜使用多线程,因为过多的线程间的切换也会降低效率,这里可以考虑用线程池

【MessageQueue程序】演示如何在子进程中创建消息队列

#include <windows.h>
#include <tchar.h>
#include <locale.h>

#define WM_MYMSG  WM_USER

HANDLE hEvent;

DWORD WINAPI ThreadProc(PVOID pvParam)
{
    MSG msg = { 0 };

    //强制系统创建一个消息队列,注释后可看到该线程没有收到任何消息
    PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE);

    if (!SetEvent(hEvent))  //创建好消息队列后,将事件重置为有信号
        return 0;

    //模拟一个耗时的初始化过程
    for (int i = 0; i < 100000000; i++);
    
    //一个简化的消息循环
    while (GetMessage(&msg,NULL,0,0)){
        _tprintf(_T("线程[ID:0x%X]收到消息-0x%04X \t时间(GetTickCount值)- %u\n"), 
                   GetCurrentThreadId(),msg.message,msg.time);
    }

    //执行到这里表示收到的是WM_QUIT消息
    _tprintf(_T("线程[ID:0x%X]收到退出消息-0x%04X \t时间(GetTickCount值)- %u\n"),
             GetCurrentThreadId(), msg.message, msg.time);

    return msg.wParam;
}

int _tmain()
{
    _tsetlocale(LC_ALL, _T("chs"));
    //创建同步事件
    hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    if (hEvent == NULL)
        return 0;

    DWORD dwThreadID = 0;
    HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadID);
    //Sleep(100); //注释掉此句,以下两条消息可能会收不到
    //以下两个消息可能收不到,因为新线程默认没有消息队列
    PostThreadMessage(dwThreadID, WM_MYMSG + 1, 0, 0);
    PostThreadMessage(dwThreadID, WM_MYMSG + 2, 0, 0);

    WaitForSingleObject(hEvent, INFINITE); //等待子线程创建好消息队列
    CloseHandle(hEvent);

    //消息队列己经建立,此时发送消息将会成功!
    PostThreadMessage(dwThreadID, WM_MYMSG + 3, 0, 0);
    PostThreadMessage(dwThreadID, WM_MYMSG + 4, 0, 0);
    
    //强制切换到新线程去执行,其实可以不必这样做,这里演示切换线程
    //以便让消息到达的时间有差异
    Sleep(100);

    PostThreadMessage(dwThreadID, WM_MYMSG + 5, 0, 0);
    PostThreadMessage(dwThreadID, WM_MYMSG + 6, 0, 0);

    //向新线程发送退出消息
    PostThreadMessage(dwThreadID, WM_QUIT, (WPARAM)GetCurrentThreadId(), 0);

    //等待新线程退出
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
    _tsystem(_T("PAUSE"));
    return 0;
}

6.2 主线程

(1)进程的入口函数,从本质上看就是主线程的入口函数。在C\C++下是WinMainCRTStartup

(2)主线程是进程内第1个可执行的线程实体,它可以用来创建别的线程。

(3)主线程退出后,进程也会退出(因为VS嵌入的入口函数会调用ExitProcess终止其它线程的执行。(当自定义入口时,这个行为就要在自定义的入口函数中自行的维护,即自定义入口函数时,那么进程将在最后一个线程退出后,才退出。因此,主线程也未必是最后一个线程!)。

6.3 线程函数(也叫线程入口函数)

(1)线程函数的原型:DWORD WINAPI ThreadProc(LPVOID lpParameter);

(2)线程函数是线程执行的起点,可以执行我们希望的任何任务

(3)当线程函数执行完毕,线程将退出,同进线程栈也会被释放,线程内核对象的使用计数递减,如果计数为0,则删除该线程内核对象。(可见线程内核对象的生命期可能长于线程本身!)

(4)线程函数必须有一个返回值,它会成为该线程的退出代码。其他线程可以用GetExitCodeThread来检查线程是否己终止运行,并进一步判断其退出代码。

(5)线程函数应尽可能使用函数参数和局部变量。因为静态变量或全局变量,多线程时可能因同时访问这些变量而要进行额外的同步。由函数参数和局部变量是在线程栈上创建的,不会出现多线程同时访问的问题。

6.4 CreateThread函数

参数

描述

psa

指向一个SECURITY_ATTRIBUTES结构体。使用默认安全属性时传入NULL

cbStackSize

①用于指定线程初始时的栈大小,通常传入0即可,此时系统会使用一个合适的大小。默认是1MB(保存在PE文件中!

②线程栈溢出时,产生异常,这可以用来捕获代码中无穷递归bug。若没限制耗尽进程所有的地址空间。

pfnStartAddr

新线程入口函数的地址(注意:新线程和调用CreateThread函数的线程可以同时被执行,这是windows抢占式的特点)

pvParam

传给线程入口函数的参数,可以是一个数值或一个结构体

dwCreateFlags

0——创建后立即执行;CREATE_SUSPENDED——创建后挂起,并不执行

pdwThreadId

得到新线程ID

返回值

成功——线程内核对象的句柄;失败——NULL

【CreateThread程序】用来说明线程调度是随机的

#include <windows.h>
#include <tchar.h>
#include <strsafe.h>
#include <locale.h>

#define MAX_THREADS  10  //最大线程数
DWORD WINAPI MyThreadFunc(LPVOID lpParam);
void  ErrorHandler(LPTSTR lpszFunction);

//自定义线程数据
typedef struct _tagMyData
{
    int val1;
    int val2;
}MYDATA,*PMYDATA;

int _tmain()
{
    _tsetlocale(LC_ALL, _T("chs"));

    PMYDATA pDataArray[MAX_THREADS];
    HANDLE  hThreadArray[MAX_THREADS];

    _tprintf(_T("以下10个线程是按顺序创建的,但线程的调度是随机\n"));
    //循环创建10个线程
    for (int i = 0; i < MAX_THREADS;i++)
    {
        pDataArray[i] = (PMYDATA)malloc(sizeof(MYDATA));
        pDataArray[i]->val1 = i;
        pDataArray[i]->val2 = i + 100;

        hThreadArray[i] = CreateThread(NULL, 0, MyThreadFunc, pDataArray[i], 
                                      0,NULL);
        if (hThreadArray[i] == NULL)
        {
            ErrorHandler(_T("CreateThread"));
            ExitProcess(3);
        }    
    }
    //等待所有线程退出
    WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE);
    for (int i = 0; i< MAX_THREADS;i++)
    {
        CloseHandle(hThreadArray[i]);
        if (pDataArray[i] != NULL)
            free(pDataArray[i]);
    }

    _tsystem(_T("PAUSE"));
    return 0;
}

//线程函数
DWORD WINAPI MyThreadFunc(LPVOID lpParam)
{
    PMYDATA pMyData = (PMYDATA)lpParam;
    _tprintf(_T("Parameters = %d,%d\n"),pMyData->val1,pMyData->val2);
    return 0;
}

void ErrorHandler(LPTSTR lpszFunction)
{
    LPVOID lpMsgBuf;
    DWORD dwError = GetLastError();
    FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM |
                  FORMAT_MESSAGE_IGNORE_INSERTS,
                  NULL,dwError,
                  MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT),
                  (LPTSTR)&lpMsgBuf,
                  0,NULL);
    _tprintf(_T("%s failed with error %d:%s"),lpszFunction,dwError,lpMsgBuf);
    LocalFree(lpMsgBuf);
}

【SuspendedCreate程序】用来创建并挂起的线程

#include <windows.h>
#include <tchar.h>

DWORD WINAPI ThreadFunction(LPVOID lpParam)
{
    _tprintf(_T("Thread(0x%0X) Runing!\n"), GetCurrentThreadId());
    return 0;
}

int _tmain()
{
    //创建并挂起新线程
    HANDLE hThread = CreateThread(NULL, 0,
                   ThreadFunction, NULL,
                   CREATE_SUSPENDED,  //挂机线程
                   NULL);
    _tprintf(_T("Thread Created!\n"));
    ResumeThread(hThread);
    Sleep(5); //如果在这里睡眠,将改变ThreadResume与线程函数里输入语句的顺序!
    _tprintf(_T("Thread Resume!\n"));
    
    

    CloseHandle(hThread);
    _tsystem(_T("PAUSE"));
    return 0;
}

6.5 终止运行线程

(1)4种终止线程的方式

终止方式

描述

线程函数返回

强烈推荐 ,这是保证所有资源被正确清理的唯一方式!可以确保以下工作正确执行。

①该函数中的所有C++对象被正确析构。②正确释放线程栈;③把线程退出代码设为函数的返回值;④递减内核对象的计数。

ExitThread

①“杀死主调线程”,操作系统将清理该线程使用的所有操作系统资源(包括线程堆栈

②可以指定dwExitCode为线程的退出代码;③C\C++资源不会被销毁

TerminateThread

杀死任何线程;②线程内核对象减1;③不销毁线程堆栈,微软故意这样做,是为了保证其他线程还可以访问被“杀死”线程栈上的值,该堆栈会等到进程结束时才被释放。③该函数是异步的,函数返回时并不保证另一线程被终止。可用WaitForSingleObject判断线程是否终止。

④将不会通知DLLMain函数某个线程退出,可能导致资源无法释放。

进程终止运行时

①ExitProcess或TerminateProcess会终止进程中所有进程,同时释放资源。

②这两个函数就好象为每个线程调用TerminateThread,所以C++对象的析构不会被调用,数据不会回写磁盘……

(2)线程终止运行时

  ①线程拥有的所有用户对象句柄被释放(如窗口和钩子句柄)

  ②线程退出代码从STILL_ACTIVE变成传给ExitThread或TerminateThread参数的退出代码。

  ③线程内核对象的状态变为触发状态,线程内核对象的使用计数减1

  ④如果线程是进程的最后一个活动线程,则进程也被终止。

6.6 线程内幕

(1)线程内部运行机制

 

  ①使用计数:CreateThread创建内核对象,使用计数初始值为2(注意:这要求对象的销毁须等线程返回并且关闭从CreateThread返回的对象句柄)

  ②暂停计数:初始化时设为1。但当线程完成初始化后,系统检查CREATE_SUSPENDED标志是否被设置。如果没被设置,则递减1,从而变为0。这意味着线程可以开始执行了

  ③退出代码为STILL_ACTIVE,对象状态为未触发状态。

  ④分配线程栈,将分别将pvParam和线程函数的地址pfnStartAddr压入栈中。

  ⑤线程上下文(CPU寄存器状态):保存在线程内核对象中,其中SP指向栈顶(即pfnStartAddr),IP指向RtlUserThreadStart函数(NTDLL.dll中)

(2)RtlUserThreadStart函数执行的操作

/*该函数是新线程真正开始执行的地方(而不是线程函数),虽然该函数有两个参数,有但这并不意味该函数是被其他函数调用的(即不要认为新线程开始执行还要再还上层去找),系统在初始化线程时,这两个参数会被操作系统显式写入线程栈中(但有的CPU架构在传这两个参数时是用寄存器的),所以该函数并没有被其他函数调用,是线程真正开始的地方
*/

VOID RTLUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam)
{
    __try
    {
        //①调用“线程函数”,并传入CreateThread传过来的pvParam参数。
        //②退出时调用ExitThread,该函数会使线程内核对象计数递减,并设置退出代码为“线程函数”返回值。
        //③因调用的是ExitThread或ExitProcess退出线程的,这意味着线程永远不会退出RtlUserThreadStart函数,它始络在其内部“消亡”,因此该函数
        //的返回值为VOID,表示永远不会返回。
        //④因为该函数不会返回,而且线程栈中也没有其返回地址(因为没有被其他函数调用),如果在没有强行“杀死”线程的前提下尝试返回,
        //RtlUserThreadStart将返回到某个随机的内存位置
        ExitThread((pfnStartAddr(pvParam)); //回调“线程函数”,并传入pvParam参数
    }
    __except (UnhandleExceptionFilter(GetExceptionInformation()))
    {
        ExitProcess(GetExceptionCode());//线程函数调用出错,则直接退出进程!
    }
    //该函数永远不会返回(因为在ExitThread或ExitProcess中退出了)
}

6.7 C/C++运行库注意事项

6.7.1 _beginThreadex的内部实现

(1)_beginThreadex函数

_CRTIMP uintptr_t __cdecl _beginthreadex(
    void *security,
    unsigned stacksize,
    unsigned(__stdcall * initialcode) (void *),
    void * argument,
    unsigned createflag,
    unsigned *thrdaddr
    )
{
    //_tiddata是个结构体,是为每线程独享的数据块,(在mtdll.h定义中)
    //他是从C\C++运行库的堆上分配的,传给_beginthreadex的线程函数和pvParam
    //参数都保存在这个数据块中,同时该结构还保存C\C++运行库中可能导致线程不安全
    //的那些函数中的静态变量(如strok函数使用了依赖于静态变量)
    _ptiddata ptd;               /* 指向每线程数据块指针(使用TLS技术) */
    uintptr_t thdl;              /* 线程句柄 */
    unsigned long err = 0L;      /* 从GetLastError()返回的错误代码 */
    unsigned dummyid;            /* 假的线程ID*/

    /* validation section 检查initialcode(线程函数指针)是否为NULL */
    _VALIDATE_RETURN(initialcode != NULL, EINVAL, 0);

    //在C\C++运行库的堆上分配一个_tiddata结构的内存,并赋值给ptd指针
    if ((ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL)
        goto error_return;

    //初始化_tiddata结构体
    _initptd(ptd, _getptd()->ptlocinfo);

    ptd->_initaddr = (void *)initialcode;  //线程函数指针
    ptd->_initarg = argument;              //线程函数的参数
    ptd->_thandle = (uintptr_t)(-1);       //线程句柄(伪句柄)

    //确保传入CreateThread函数的thrdaddr(即用来接收线程ID的指针)不为空
    if (thrdaddr == NULL) //判断是否需要返回线程ID号
        thrdaddr = &dummyid;

    //调用CreateThread函数来创建新线程
    if (thdl = (uintptr_t)CreateThread((LPSECURITY_ATTRIBUTES)security,
        stacksize,
        _threadstartex,  //在_beginthreadex内部,线程函数的地址被修改成_threadstartex
        (LPVOID)ptd,     //将_tiddata数据块的指针传给线程函数
        createflag,
        (LPDWORD)thrdaddr) //要返回的线程ID指针
        == (uintptr_t)0)
    {
        err = GetLastError();
        goto error_return;
    }

    //创建成功,返回线程句柄
    return(thdl);

    //创建线程错误时的处理
error_return:
    //回收由_calloc_crt()申请的_tiddata块
    _free_crt(ptd);

    //校正错误代码(可以使用GetLastError()得到错误代码)
    if (err != 0L)
        _dosmaperr(err);

    return((uintptr_t)0); //返回值为NULL的无效句柄
}

//_threadstartex() -新线程开始的地方
static unsigned long WINAPI _threadstartex(void * ptd)
{
    _ptiddata _ptd;   /*从CreateThread传入的线程函数参数  */

    //检查动态库中的THREAD_ATTACH调用中是否初始化ptd
    if ((_ptd = (_ptiddata)__crtFlsGetValue(__get_flsindex())) == NULL)
    {

        //将tiddata数据库与线程关联起来
        if (!__crtFlsSetValue(__get_flsindex(), ptd))
            ExitThread(GetLastError());

        //将线程ID保存在_tiddata数据块中。(父线程在调用了CreateThread
        //以后不能再设置线程ID这个字段了,因为子线程可能己经运行完毕,
        //并释放了_tiddata数据块)
        ((_ptiddata)ptd)->_tid = GetCurrentThreadId(); //保存父线程ID
        _ptd = ptd;
    } else
    {
        _ptd->_initaddr = ((_ptiddata)ptd)->_initaddr;
        _ptd->_initarg = ((_ptiddata)ptd)->_initarg;
        _ptd->_thandle = ((_ptiddata)ptd)->_thandle;

        _freefls(ptd); //如果动态库中己经初始化了ptd,由释放ptd
        ptd = _ptd;   //将ptd赋新的值_ptd
    }

    _ptd->_initapartment = __crtIsPackagedApp();
    if (_ptd->_initapartment)
    {
        _ptd->_initapartment = _initMTAoncurrentthread();
    }

    //调用Helper函数
    _callthreadstartex();

    //以下将永远不会执行,因为线程最终会终止在_callthreadstartex函数内部!
    return(0L);
}

//
static void _callthreadstartex(void)
{
    _ptiddata ptd;           /* 指向_tiddata指针 */

    ptd = _getptd(); //从TLS中获取指向_tiddata的指针

    __try {
        //在这里调用我们的线程函数(函数指针_initaddr字段,参数在_initarg中)
        //线程函数结束后,将返回值并为_endthreadex的参数来调用_endthreadex以
        //便结束线程(注意,很明显,线程会“死”在_callthreadstartex中)
        _endthreadex(
            ((unsigned (__CLR_OR_STD_CALL *)(void *))(((_ptiddata)ptd)->_initaddr))
            (((_ptiddata)ptd)->_initarg));
    }
    __except (_XcptFilter(GetExceptionCode(), GetExceptionInformation()))
    {
       //可能永远不会被执行!
        _exit(GetExceptionCode());

    } /* end of _try - _except */
}

【关于 _beginthreadex说明的几点】

  ①因为_beginthreadex和_endthreadex是CRT线程函数,所以必须注意编译选项runtimelibaray的选择,使用MT或MTD(MultiThreaded , Debug MultiThreaded)

  ②每个线程均获得由C/C++运行期库的堆栈分配的自己的tiddata内存结构。(tiddata结构位于Mtdll.h文件中的VisualC++源代码中)

  ③传递给_beginthreadex的线程函数的地址保存在tiddata内存块中。传递给该函数的参数也保存在该数据块中

  ④_beginthreadex确实从内部调用CreateThread,因为这是操作系统了解如何创建新线程的唯一方法

  ⑤当调用CreatetThread时,它被告知通过调用_threadstartex而不是pfnStartAddr来启动执行新线程。还有,传递给线程函数的参数是tiddata结构而不是pvParam的地址即新线程首先执行RtlUserStartAddr,然后跳转进入_threadstartex。

//(2)_endthreadex

void __cdecl _endthreadex(
    unsigned retcode
    )
{
    _ptiddata ptd;           /* 指向_tiddata的指针 */
    HANDLE handle = NULL;

    ptd = _getptd_noexit(); //获得指向_tiddata的指针

    //清除_tiddata块中的floating-point
    if (ptd) {
        if (ptd->_initapartment)
            _uninitMTAoncurrentthread();

        _freeptd(ptd); //释放tiddata结构体,内存被正确释放!
    }

    //退出线程
    ExitThread(retcode);
}

【关于 _endthreadex说明的几点】 

  ①C运行期库的_getptd函数内部调用操作系统的TlsGetValue函数,该函数负责检索主调线程的tiddata内存块的地址。

  ②然后该数据块被释放,而操作系统的ExitThread函数被调用,以便真正撤消该线程。当然,退出代码要正确地设置和传递。

 

6.7.2 使用_beginthreadex而不要用CreateThread函数

(1)如果使用CreateThread而不是_beginthreadex来创建线程会发生什么情况?

  ①当线程调用一个需要_tiddata结构的C\C++运行库函数(如strok)时,这个运行库函数会检查到_tiddata块为NULL,而会自动创建一个与主调线程关联的_tiddata块,这样做的目的是保证该库函数能正常运行。(注意,以后调用的任何C\C++运行库都可以使用这个_tiddata块,而无需重复创建!)。

  ②但因CreateThread是API函数,不会像_endthreadex那样去销毁这个数据块,因此可能造成内存泄漏

  ③如果线程使用了C\C++运行库的signal函数,则会导致整个进程终止,因为CreateThread函数没有为这个函数准备结构化异常处理帧(SEH)

(2)也不要使用_beginthread/_endthread函数(注意,函数名后不带ex

  ①_beginthread函数参数少,没有CREATE_SUSPENDED,也不能获取线程ID值

  ②_endthread是无参的,意味着线程的退出代码被硬编码为0

  ③_endthread内部会调用CloseHandle来关闭新线,但这会造成潜在的危险!如:

  DWORD dwExitCode;

  HANDLE hThread  = _beginthread(…);          //该函数会使新线程立即运行!

  GetExitCodeThread(hThread,&dwExitCode); //但子线程可能在该语句之前就结束

                                                                 //但_endthread内部调用了CloseHandle使hThread无效!

  CloseHandle(hThread);  //这里重复关闭hTread就会出错

_endthreadex函数内部不会关闭线程句柄,因此以上代码不会有bug

6.8 了解自己的身份

(1)伪句柄:

功能

函数

备注

获取当前进程的句柄值

GetCurrentProcess

永远都是0xFFFFFFFF

获取当前线程的句柄值

GetCurrentThread

永远都是0xFFFFFFFE

   说明:

  ①伪句柄不会在主调进程句柄表中新建句柄项,故不会影响相应内核对象的使用计数

  ②如果调用CloseHandle关闭伪句柄该参数会被忽略,被返回FALSE。调用GetLastError将返回ERROR_INVALID_HANDLE。

  ③A线程的伪句柄作为参数传递给B线程时,该参数不能正确表示A线程,相反,在B线程中,该句柄其实代表的是B(因为B的伪句柄也是0xFFFFFFFE)

(2)将伪句柄转换为真实的句柄:DubplicateHandle函数

(3)获取线程、进程运行的CPU时间

  ①GetThreadTime

  ②GetProcessTime