• 多线程学习笔记1


    CreateThread, AfxBeginThread,_beginthread, _beginthreadex的区别


    CreateThread是Windows的API函数(SDK函数的标准形式, 直截了当的创建方式,任何场合都可以使用),提供操作系统级别的创建线程的操作,且仅限于工作者线程。不调用MFC和RTL的函数时,可以用 CreateThread,其它情况不要轻易。在使用的过程中要考虑到进程的同步与互斥的关系(防止死锁)。线程函数定义为:DWORD WINAPI _yourThreadFun(LPVOID pParameter)。但它没有考虑:
    (1)C Runtime中需要对多线程进行纪录和初始化,以保证C函数库工作正常(典型的例子是strtok函数)。
    (2)MFC也需要知道新线程的创建,也需要做一些初始化工作(当然,如果没用MFC就没事了)。  

    AfxBeginThread:MFC中线程创建的MFC函数,首先创建了相应的 CWinThread对象,然后调用CWinThread::CreateThread,   在CWinThread::CreateThread中,完成了对线程对象的初始化工作,然后,调用 _beginthreadex(AfxBeginThread相比较更为安全)创建线程。它简化了操作或让线程能够响应消息,即可用于界面线程,也可以用 于工作者线程,但要注意不要在一个MFC程序中使用_beginthreadex()或CreateThread()。线程函数定义为:UINT _yourThreadFun(LPVOID pParam)

    _beginthreadex:MS对C Runtime库的扩展SDK函数,首先针对C Runtime库做了一些初始化的工作,以保证C Runtime库工作正常。然后,调用CreateThread真正创建线程。 仅使用Runtime Library时,可以用_BegingThread。

    小节:实际上,这三个函数之间存在一定的调用关系,第一个纯粹一些,后两个完成自己相应的工作之后,调用前者实现线程的创建。其中CreateThread是由操作系统提供的接口,而AfxBeginThread和_BeginThread则是编译器对它的封装。

    小节:用_beginthreadex()、_endthreadex函数应该是最佳选择,且都是C Run-time Library中的函数,函数的参数和数据类型都是C Run-time Library中的类型,这样在启动线程时就不需要进行Windows数据类型和C Run-time Library中的数据类型之间的转化,从而,减低了线程启动时的资源消耗和时间的消耗。但使用_beginthread,无法创建带有安全属性的新线程,无法创建暂停的线程,也无法获得 线程ID,_endthread的情况类似,它不带参数,这意味这线程的退出代码必须硬编码为0。

    小节:MFC也是C++类库(只不过是Microsoft的C++类库,不是标准的C++类库),在MFC中也封装了new和delete两中运算符,所以用到new和delete的地方不一定非要使用_beginthreadex() 函数,用其他两个函数都可以。

    _beginthreadex和_beginthread在回调入口函数之前进行一些线程相关的CRT的初始化操作。
    CRT的函数库在线程出现之前就已经存在,所以原有的CRT不能真正支持线程,
    这也导致了许多CRT的函数在多线程的情况下必须有特殊的支持,不能简单的使用CreateThread就OK。




     如果用MFC编程,不要用CreateThread,如果只是使用Runtime Library,用_BegingThread,总之,不要轻易使用CreateThread。这是因为在MFC和RTL中的函数有可能会用到些它们所封 装的公用变量,也就是说AfxBeginThread和_BeginThread都有自己的启动代码是CreateThread所没有的。在用 CreateThread所创建的线程中使用MFC的类和RTL函数就有可能出现问题。如果你是用汇编编写win32程序并且在线程函数中也不调用MFC 和RTL的函数,那用CreateThread就没问题,或者你虽然是用C写线程函数,但你很小心没调用RTL函数也不会有问题。

      CreateThread是由操作系统提供的接口,而AfxBeginThread和_BeginThread则是编译器对它的封装。

      在可能的情况下,不要调用_beginthread,而应该调用_beginthreadex。以及对应的 _endthreadex。这都是C++运行期函数。但是使用_beginthread,无法创建带有安全属性的新线程,无法创建暂停的线程,也无法获得 线程ID,_endthread的情况类似,它不带参数,


      这意味这线程的退出代码必须硬编码为0。这两个函数在_beginthreadex和_endthreadex中进行调用。CreateThread不要进行直接调用。




        具体说来,CreateThread这个 函数是windows提供给用户的 API函数,是SDK的标准形式,在使用的过程中要考虑到进程的同步与互斥的关系,进程间的同步互斥等一系列会导致操作系统死锁的因素,用起来比较繁琐一 些,初学的人在用到的时候可能会产生不可预料的错误,建议多使用AfxBeginThread。

        AfxBeginThread是编译器对原来的CreateThread函数的封装,用 与MFC编程(当然,只要修改了项目属性,console和win32项目都能调用)而_beginthread是C的运行库函数。 

         在使用AfxBeginThread时,线程函数的定义为:

              UINT   _yourThreadFun(LPVOID   pParam)参数必须如此

         在使用CreateThread时,线程的函数定义为: 

             DWORD  WINAPI  _yourThreadFun(LPVOID pParameter)。
         两个的实质都是一样的,不过AfxBeginThread返回一个CWinThread的指针,就是说他会new一个CWinThread对象,而且这个对象是自动删除的(在线程
    运行结束时),给我们带来的不便就是无法获得它的状态,因为随时都有可能这个指针指向的是一个已经无效的内存区 域,所以使用时(如果需要了解它的运行状况的话)首先CREATE_SUSPENDED让他挂起,然后m_bAutoDelete=FALSE,接着才 ResumeThread,最后不要了delete那个指针。    

         CreatThread就方便多了,它返回的是一个句柄,如果你不使用CloseHandle的话就可以通过他安全的了解线程状态,最后不要的时候CloseHandle,Windows才会释放资源,所以我一般使用CreatThread,方便


         如果用MFC编程,不要用CreateThread,如果只是使用Runtime Library,用_BegingThread,总之,不要轻易使用CreateThread。这是因为在MFC和RTL中的函数有可能会用到些它们所封装的公用变量,也就是说AfxBeginThread和_BeginThread都有自己的启动代码是CreateThread所没有的。在用 CreateThread所创建的线程中使用MFC的类和RTL函数就有可能出现问题。如果你是用汇编编写win32程序并且在线程函数中也不调用MFC 和RTL的函数,那用CreateThread就没问题,或者你虽然是用C写线程函数,但你很小心没调用RTL函数也不会有问题。

      CreateThread是由操作系统提供的接口,而AfxBeginThread和_BeginThread则是编译器对它的封装。

      在可能的情况下,不要调用_beginthread,而应该调用_beginthreadex。以及对应的 _endthreadex。这都是C++运行期函数。但是使用_beginthread,无法创建带有安全属性的新线程,无法创建暂停的线程,也无法获得 线程ID,_endthread的情况类似,它不带参数,


      这意味这线程的退出代码必须硬编码为0。这两个函数在_beginthreadex和_endthreadex中进行调用。CreateThread不要进行直接调用。




    1. 进程与线程有那些区别和联系?
          每个进程至少需要一个线程。
          进程由两部分构成:进程内核对象,地址空间。线程也由两部分组成:线程内核对象,操作系统用它来对线程实施管理。线程堆栈,用于维护线程在执行代码时需要的所有函数参数和局部变量。
          进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。
          如果在单进程环境中,有多个线程正在运行,那么这些线程将共享单个地址空间。这些线程能够执行相同的代码,对相同的数据进行操作。这些线程还能共享内核对象句柄,因为句柄表依赖于每个进程而不是每个线程存在。
          进程使用的系统资源比线程多得多。实际上,线程只有一个内核对象和一个堆栈,保留的记录很少,因此需要很少的内存。因此始终都应该设法用增加线程来解决编程问题,避免创建新的进程。但是许多程序设计用多个进程来实现会更好些。

    2. 如何使用_beginthreadex函数?
          使用方法与CreateThread函数相同,只是调用参数类型需要转换。

    3. 如何使用CreateThread函数?
          当CreateThread被调用时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。使用时应当 注意在不需要对线程内核进行访问后调用CloseHandle函数关闭线程句柄。因为CreateThread函数中使用某些C/C++运行期库函数时会 有内存泄漏,所以应当尽量避免使用。
    参数含义:
      lpThreadAttributes  如果传递NULL该线程使用默认安全属性。如果希望所有的子进程能够继承该线程对象的句柄,必须将它的bInheritHandle成员被初始化为TRUE。
      dwStackSize  设定线程堆栈的地址空间。如果非0,函数将所有的存储器保留并分配给线程的堆栈。如果是0,CreateThread就保留一个区域,并且将链接程序嵌入.exe文件的/STACK链接程序开关信息指明的存储器容量分配给线程堆栈。
      lpStartAddress  线程函数的地址。
      lpParameter  传递给线程函数的参数。
      dwCreationFlags  如果是0,线程创建后立即进行调度。如果是CREATE_SUSPENDED,系统对它进行初始化后暂停该线程的运行。
      LpThreadId  用来存放系统分配给新线程的ID。

    4. 如何终止线程的运行?
    (1)   线程函数返回(最好使用这种方法)。
      这是确保所有线程资源被正确地清除的唯一办法。如果线程能够返回,就可以确保下列事项的实现:
      在线程函数中创建的所有C++对象均将通过它们的撤消函数正确地撤消。操作系统将正确地释放线程堆栈使用的内存。
      系统将线程的退出代码设置为线程函数的返回值。系统将递减线程内核对象的使用计数。
    (2)   调用ExitThread函数(最好不要使用这种方法)。
      该函数将终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。但是,C++资源(如C++类对象)将不被撤消。
    (3)    调用TerminateThread函数(应该避免使用这种方法)。
       TerminateThread能撤消任何线程。线程的内核对象的使用计数也被递减。TerminateThread函数是异步运行的函数。如果要确切 地知道该线程已经终止运行,必须调用WaitForSingleObject或者类似的函数。当使用返回或调用ExitThread的方法撤消线程时,该 线程的内存堆栈也被撤消。但是,如果使用TerminateThread,那么在拥有线程的进程终止运行之前,系统不撤消该线程的堆栈。
    (4)    包含线程的进程终止运行(应该避免使用这种方法)。由于整个进程已经被关闭,进程使用的所有资源肯定已被清除。就像从每个剩余的线程调用 TerminateThread一样。这意味着正确的应用程序清除没有发生,即C++对象撤消函数没有被调用,数据没有转至磁盘等等。一旦线程不再运行, 系统中就没有别的线程能够处理该线程的句柄。然而别的线程可以调GetExitcodeThread来检查由hThread标识的线程是否已经终止运行。 如果它已经终止运行,则确定它的退出代码。

    5. 为什么不要使用_beginthread函数和_endthread函数?
      与_beginthreadex函数相比参数少,限制多。无法创建暂停的线程,无法取得线程ID。_endthread函数无参数,线程退出代码必须为0。还有_endthread函数内部关闭了线程的句柄,一旦退出将不能正确访问线程句柄。

    6. 如何对进程或线程的内核进行引用?
    HANDLE GetCurrentProcess(  );
    HANDLE GetCurrentThread(  );
       这两个函数都能返回调用线程的进程的伪句柄或线程内核对象的伪句柄。伪句柄只能在当前的进程或线程中使用,在其它线程或进程将不能访问。函数并不在创建 进程的句柄表中创建新句柄。调用这些函数对进程或线程内核对象的使用计数没有任何影响。如果调用CloseHandle,将伪句柄作为参数来传递,那么 CloseHandle就会忽略该函数的调用并返回FALSE。
    DWORD GetCurrentProcessId(  );
    DWORD GetCurrentThreadId(  );
      这两个函数使得线程能够查询它的进程的唯一ID或它自己的唯一ID。

    7. 如何将伪句柄转换为实句柄?
    HANDLE hProcessFalse = NULL;
    HANDLE hProcessTrue = NULL;
    HANDLE hThreadFalse = NULL;
    HANDLE hThreadTrue = NULL;

    hProcessFalse = GetCurrentProcess(  );
    hThreadFalse = GetCurrentThread(  );
    取得线程实句柄:
    DuplicateHandle( hProcessFalse, hThreadFalse, hProcessFalse, &hThreadTrue, 0, FALSE, DUPLICATE_SAME_ACCESS );
    取得进程实句柄:
    DuplicateHandle( hProcessFalse, hProcessFalse, hProcessFalse, &hProcessTrue, 0, FALSE, DUPLICATE_SAME_ACCESS );
    由于DuplicateHandle会递增特定对象的使用计数,因此当完成对复制对象句柄的使用时,应该将目标句柄传递给CloseHandle,从而递减对象的使用计数。

    8. 在一个进程中可创建线程的最大数是得多少?
      线程的最大数取决于该系统的可用虚拟内存的大小。默认每个线程最多可拥有至多1MB大小的栈的空间。所以,至多可创建2028个线程。如果减少默认堆栈的大小,则可以创建更多的线程。

    线程的调度、优先级和亲缘性
    9. 如何暂停和恢复线程的运行?
      线程内核对象的内部有一个值指明线程的暂停计数。当调用 CreateProcess或CreateThread函数时,就创建了线程的内核对象,并且它的暂停计数被初始化为1。因为线程的初始化需要时间,不能 在系统做好充分的准备之前就开始执行线程。线程完全初始化好了之后,CreateProcess或CreateThread要查看是否已经传递了 CREATE_SUSPENDED标志。如果已经传递了这个标志,那么这些函数就返回,同时新线程处于暂停状态。如果尚未传递该标志,那么该函数将线程的 暂停计数递减为0。当线程的暂停计数是0的时候,除非线程正在等待其他某种事情的发生,否则该线程就处于可调度状态。在暂停状态中创建一个线程,就能够在 线程有机会执行任何代码之前改变线程的运行环境(如优先级)。一旦改变了线程的环境,必须使线程成为可调度线程。方法如下:
    hThread = CreatThread( ……,CREATE_SUSPENDED,…… );

    bCreate = CreatProcess( ……,CREATE_SUSPENDED,……,pProcInfo );
    if( bCreate != FALSE )
    {
     hThread = pProcInfo.hThread;
    }
    ……
    ……
    ……
    ResumeThread( hThread );
    CloseHandle( hThread );
    ResumeThread成功,它将返回线程的前一个暂停计数,否则返回0xFFFFFFFF。
       单个线程可以暂停若干次。如果一个线程暂停了3次,它必须恢复3次。创建线程时,除了使用CREATE_SUSPENDED外,也可以调用 SuspendThread函数来暂停线程的运行。任何线程都可以调用该函数来暂停另一个线程的运行(只要拥有线程的句柄)。线程可以自行暂停运行,但是 不能自行恢复运行。与ResumeThread一样,SuspendThread返回的是线程的前一个暂停计数。线程暂停的最多次数可以是 MAXIMUM_SUSPEND_COUNT次。SuspendThread与内核方式的执行是异步进行的,但是在线程恢复运行之前,不会发生用户方式的 执行。调用SuspendThread时必须小心,因为不知道暂停线程运行时它在进行什么操作。只有确切知道目标线程是什么(或者目标线程正在做什么), 并且采取强有力的措施来避免因暂停线程的运行而带来的问题或死锁状态,SuspendThread才是安全的。

    10. 是否可以暂停和恢复进程的运行?
         对于Windows来说,不存在暂停或恢复进程的概念,因为进程从来不会被安排获得CPU时间。不过Windows确实允许一个进程暂停另一个进程中的 所有线程的运行,但是从事暂停操作的进程必须是个调试程序。特别是,进程必须调用WaitForDebugEvent和 ContinueDebugEvent之类的函数。由于竞争的原因,Windows没有提供其他方法来暂停进程中所有线程的运行。

    11. 如何使用sleep函数?
     系统将在大约的指定毫秒数内使线程不可调度。Windows不是个实时操作系统。虽然线程可能在规定的时间被唤醒,但是它能否做到,取决于系统中还有什么操作正在进行。
       可以调用Sleep,并且为dwMilliseconds参数传递INFINITE。这将告诉系统永远不要调度该线程。这不是一件值得去做的事情。最好 是让线程退出,并还原它的堆栈和内核对象。可以将0传递给Sleep。这将告诉系统,调用线程将释放剩余的时间片,并迫使系统调度另一个线程。但是,系统 可以对刚刚调用Sleep的线程重新调度。如果不存在多个拥有相同优先级的可调度线程,就会出现这种情况。

    12. 如何转换到另一个线程?
         系统提供了SwitchToThread函数。当调用这个函数的时候,系统要查看是否存在一个迫 切需要CPU时间的线程。如果没有线程迫切需要CPU时间,SwitchToThread就会立即返回。如果存在一个迫切需要CPU时间的线 程,SwitchToThread就对该线程进行调度(该线程的优先级可能低于调用SwitchToThread的线程)。这个迫切需要CPU时间的线程 可以运行一个时间段,然后系统调度程序照常运行。该函数允许一个需要资源的线程强制另一个优先级较低、而目前却拥有该资源的线程放弃该资源。如果调用 SwitchToThread函数时没有其他线程能够运行,那么该函数返回FALSE,否则返回一个非0值。调用SwitchToThread与调用 Sleep是相似的。差别是SwitchToThread允许优先级较低的线程运行;而即使有低优先级线程迫切需要CPU时间,Sleep也能够立即对调 用线程重新进行调度。

    13. 如何取得线程运行的时间?
    (1)   简单取得线程大概运行时间:
    DWORD dwStartTime = 0;
    DWORD dwEndTime = 0;
    DWORD dwRunTime = 0;
    dwStartTime = GetTickCount(  );
    ……
    ……
    ……
    dwEndTime = GetTickCount(  );
    dwRunTime = dwEndTime – dwStartTime;
    (2)    调用GetThreadTimes的函数:
    参数含义:
    hThread 线程句柄
    lpCreationTime 创建时间:英国格林威治时间
    lpExitTime 退出时间:英国格林威治时间,如果线程仍然在运行,退出时间则未定义
    lpKernelTime 内核时间:指明线程执行操作系统代码已经经过了多少个100ns的CPU时间
    lpUserTime 用户时间:指明线程执行应用程序代码已经经过了多少个100ns的CPU时间
    GetProcessTimes 是个类似GetThreadTimes的函数,适用于进程中的所有线程(甚至是已经终止运行的线程)。返回的内核时间是所有进程的线程在内核代码中经过的 全部时间的总和。GetThreadTimes和GetProcessTimes这两个函数在Windows98中不起作用。在Windows98中,没 有一个可靠的机制可供应用程序来确定线程或进程已经使用了多少CPU时间。

    14. 进程的优先级类有哪些?
    优先级类 标识符 描述
    实时      REALTIME_PRIORITY_CLASS 立即对事件作出响应,执行关键时间的任务。会抢先于操作系统组件之前运行。
    高       HIGH_PRIORITY_CLASS 立即对事件作出响应,执行关键时间的任务。
    高于正常    ABOVE_NORMAL_PRIORITY_CLASS 在正常优先级与高优先级之间运行(Windows2000)。
    正常      NORMAL_PRIORITY_CLASS 没有特殊调度需求
    低于正常    BELOW_NORMAL_PRIORITY_CLASS 在正常优先级与空闲优先级之间运行(Windows2000)。
    空闲      IDLE_PRIORITY_CLASS 在系统空闲时运行。
    设置方法:
    BOOL SetPriorityClass( HANDLE hProcess, DWORD dwPriority );
    DWORD GetPriorityClass( HANDLE hProcess );
    使用命令外壳启动一个程序时,该程序的起始优先级是正常优先级。如果使用Start命令来启动该程序,可以使用一个开关来设定应用程序的起始优先级。例如:
    c:/>START /LOW CALC.EXE
    Start命令还能识别/BELOWNORMAL、/NORMAL、/ABOVENORMAL、/HIGH和/REALTIME等开关。

    15. 线程的相对优先级有哪些?
    相对优先级 标识符 描述
    关键时间    THREAD_PRIORITY_TIME_CRITICAL 对于实时优先级类线程在优先级31上运行,对于其他优先级类,线程在优先级15上运行。
    最高      THREAD_PRIORITY_HIGHEST 线程在高于正常优先级上两级上运行。
    高于正常    THREAD_PRIORITY_ABOVE_NORMAL 线程在正常优先级上一级上运行。
    正常      THREAD_PRIORITY_NORMAL 线程在进程的优先级类上正常运行。
    低于正常    THREAD_PRIORITY_BELOW_NORMAL 线程在低于正常优先级下一级上运行。
    最低      THREAD_PRIORITY_LOWEST 线程在低于正常优先级下两级上运行。
    空闲      THREAD_PRIORITY_IDLE 对于实时优先级类线程在优先级16上运行对于其他优先级类线程在优先级1上运行。
    设置方法:
    BOOL SetThreadPriority( HANDLE hThread, DWORD dwPriority );
    DWORD GetThreadPriorityClass( HANDLE hThread );

    16. 如何避免系统动态提高线程的优先级等级?
      系统常常要提高线程的优先级等级,以便对窗口消息或读取磁盘等I/O事件作出响应。或 者当系统发现一个线程在大约3至4s内一直渴望得到CPU时间,它就将这个渴望得到CPU时间的线程的优先级动态提高到15,并让该线程运行两倍于它的时 间量。当到了两倍时间量的时候,该线程的优先级立即返回到它的基本优先级。下面的函数可以对系统的调度方式进行设置:
    BOOL SetProcessPriorityBoost( HANDLE hProcess, BOOL bDisableBoost );
    BOOL GetProcessPriorityBoost( HANDLE hProcess, PBOOL pbDisableBoost );
    BOOL SetThreadPriorityBoost( HANDLE hThread, BOOL bDisableBoost );
    BOOL GetThreadPriorityBoost( HANDLE hThread, PBOOL pbDisableBoost );
    SetProcessPriorityBoost负责告诉系统激活或停用进行中的所有线程的优先级提高功能,而SetThreadPriorityBoost则激活或停用各个线程的优先级提高功能。Windows98没有提供这4个函数的有用的实现代码。

  • 相关阅读:
    第一章 线性模型
    Kaggle比赛:从何着手?
    Kaggle初学者五步入门指南,七大诀窍助你享受竞赛
    3.深度学习的实用层面
    软件工程面试题
    PyQT5速成教程-4 Qt Designer实战[上]
    PyQT5速成教程-3 布局管理
    PyQT5速成教程-1 简介与环境搭建
    Anaconda 使用指南
    webpack的loader的原理和实现
  • 原文地址:https://www.cnblogs.com/kex1n/p/2286557.html
Copyright © 2020-2023  润新知