zoukankan      html  css  js  c++  java
  • 第6章 线程基础

    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 	时间(GetTickCount值)- %u
    "), 
                       GetCurrentThreadId(),msg.message,msg.time);
        }
    
        //执行到这里表示收到的是WM_QUIT消息
        _tprintf(_T("线程[ID:0x%X]收到退出消息-0x%04X 	时间(GetTickCount值)- %u
    "),
                 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)进程的入口函数,从本质上看就是主线程的入口函数。在CC++下是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个线程是按顺序创建的,但线程的调度是随机
    "));
        //循环创建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
    "),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!
    "), GetCurrentThreadId());
        return 0;
    }
    
    int _tmain()
    {
        //创建并挂起新线程
        HANDLE hThread = CreateThread(NULL, 0,
                       ThreadFunction, NULL,
                       CREATE_SUSPENDED,  //挂机线程
                       NULL);
        _tprintf(_T("Thread Created!
    "));
        ResumeThread(hThread);
        Sleep(5); //如果在这里睡眠,将改变ThreadResume与线程函数里输入语句的顺序!
        _tprintf(_T("Thread Resume!
    "));
        
        
    
        CloseHandle(hThread);
        _tsystem(_T("PAUSE"));
        return 0;
    }

    6.5 终止运行线程

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

    终止方式

    描述

    线程函数返回

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

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

    ExitThread

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

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

    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定义中)
        //他是从CC++运行库的堆上分配的,传给_beginthreadex的线程函数和pvParam
        //参数都保存在这个数据块中,同时该结构还保存CC++运行库中可能导致线程不安全
        //的那些函数中的静态变量(如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);
    
        //在CC++运行库的堆上分配一个_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结构的CC++运行库函数(如strok)时,这个运行库函数会检查到_tiddata块为NULL,而会自动创建一个与主调线程关联的_tiddata块,这样做的目的是保证该库函数能正常运行。(注意,以后调用的任何CC++运行库都可以使用这个_tiddata块,而无需重复创建!)。

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

      ③如果线程使用了CC++运行库的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

  • 相关阅读:
    A
    博弈论
    K
    快速幂
    基数排序
    计数排序
    KMP求字符串最小循环节
    二分图多重匹配
    hdu2818行列匹配+排序
    二分图行列匹配与最大匹配必须边
  • 原文地址:https://www.cnblogs.com/5iedu/p/4697161.html
Copyright © 2011-2022 走看看