340 likes | 494 Views
线程的创建和运行. 第三章 线程的创建和运行. 3.1 基本概念 3.2 线程的创建 3.3 终止线程 3.4 线程的暂停与回复 3.5 在类中使用线程. 3.1 基本概念. Windows 进程. 进程的四种入口函数 intWINAPI WindMain() | WinMainCRTStartup ¨ intWINAPI wWinMain() | wWindMainCRTStartup ¨ int__cdeclmain() | mainCRTStartup
E N D
第三章 线程的创建和运行 • 3.1 基本概念 • 3.2 线程的创建 • 3.3 终止线程 • 3.4 线程的暂停与回复 • 3.5 在类中使用线程
Windows进程 • 进程的四种入口函数 intWINAPI WindMain() | WinMainCRTStartup ¨ intWINAPI wWinMain() | wWindMainCRTStartup ¨ int__cdeclmain() | mainCRTStartup ¨ int__cdeclwMain() | wmainCRTStartup • 操作系统不直接调用入口函数, • 而是调用c/c++运行库的启动函数
操作系统启动进程 • 检索指向新进程的完整命令行的指针 • 检索指向新进程的环境变量的指针 • 对C/C++运行期的全局变量初始化 • 对C 运行期内存单元分配函数(malloc和calloc)和其他的底层输出输入例程使用的内存堆进行初始化 • 为所有全局和静态的C++对象调用构造函数
线程的构成 • 线程的内核对象,操作系统用它来对线程实施管理 • 线程堆栈,用于维护线程在执行代码时候需要的所有函数局部变量。 • 线程上下文(一组CPU寄存器状态,特别是指令指针寄存器和堆栈指针寄存器) • 指令寄存器和堆栈寄存器记录的地址都用于标志拥有线程的进程地址空间中的内存
线程和进程 • 进程比线程使用更多的系统资源,原因是它需要更多的地址空间。为进程创建一个虚拟地址空间,需要很多系统资源,同时,系统中要保留大量的记录,这也要占用大量的内存。 • 另外,dll或者exe需要加载到一个地址空间,也需要文件资源。 • 线程只需要一个内核对象和一个堆栈,保留的记录很少,因此需要很少的内存 • Windows中进程是不活泼的,进程从来不执行任何东西,进程只是线程的容器 • 应该用增加线程来解决编程问题,避免创建新的进程。
Windows线程的调度 • 抢占式操作系统必须使用某种算法来确定哪些线程应该在何时调度和运行多长时间 • Windows被称为抢占式多线程操作系统,因为一个线程可以随时停止运行,然后另外一个线程进行调度。 • 基于任务优先级的抢占式调度算法,同一优先级的任务遵循时间片轮转,并且遵循FIFO策略。 • 每隔20ms左右,windows要查看当前所有的线程内核对象,在这些内核对象中,只有某些对象被视为可以调度的对象,Windows选择可调度线程内核对象中的一个,将他加载到CPU寄存器中,然后继续运行,当系统引导时,便可以加载CPU寄存器中的线程上下文,使线程运行。 • 系统只调度可以调度的线程。实际情况是很多线程是不可调度的线程,比如一个暂停运行的线程(可以在创建线程的时候,直接指定这个线程是暂停的。)比如一个正在等待某些事情发生的线程。 • CPU不给无事可作的线程分配CPU时间。
Windows线程的优先级 • 每个线程都会被赋予一个从0-31的优先级号码 • 只要是高优先级的线程是可以调度的,系统绝对不会调度低优先级的(Starvation) • 系统引导的时候创建特殊的线程——0页线程,其优先级为0,当系统中没有任何其他线程运行时,0页线程负责将系统中所有的空闲RAM页面置0 • 进程内使用线程相对的优先级:空闲、最低、低于正常、正常、高于正常、最高、关键时间 • 进程也根据具体情况被分为5个进程优先级类:空闲、低于正常、正常、高于正常、实时 • 线程的实际优先级是进程优先级类和进程内线程相对优先级的组合 • 正常优先级类的进程的基本优先级是5, 进程内正常优先级线程的优先级是8,所以一个正常进程中的正常优先级线程的真正优先级是13
何时创建一个进程的主线程 • 线程用于描述进程中的运行路径。每当进程被初始化,系统就要创建一个主线程。该线程和C/C++运行库的启动代码一道开始运行,启动代码则调用进入点函数。并且继续运行直到进入点函数返回并且C/C++运行库的启动代码调用ExitProcess为止
线程进入点函数 • 每个线程必须有一个进入点函数,线程从这个进入点开始运行。 • 一个进程的主线程的进入点函数 • main, wmain, WinMain, wWinMain • 一个辅助线程的进入点函数:例如 DWORD WINAPI ThreadFunc(LPVOID pvParam) { DWORD dwResult= 0; ….. return dwResult. }
CreateThread函数 • 在一个已经运行的线程中创建辅助线程 HANDLE CreateThread( LPSECURITY_ATTRIBUTESlpThreadAttributes, // 安全性,缺省NULL SIZE_TdwStackSize, // initial stack size,0表示和调用线程一样大小 LPTHREAD_START_ROUTINElpStartAddress, // thread function LPVOIDlpParameter, // threadargument,传递给线程的参数,NULL则不传参 DWORDdwCreationFlags, // creation option,用于确定线程创建后是否立即运行,0则立即运行 LPDWORDlpThreadId // [out] thread identifier,NULL则线程标识不返回 ); [in] Specifies additional flags that control the creation of the thread. If the CREATE_SUSPENDED flag is specified, the thread is created in a suspended state, and will not run until the ResumeThread function is called. If this value is zero, the thread runs immediately after creation. 自定义的线程入口函数(函数名可以自定义) DWORD WINAPI ThreadProc( LPVOIDlpParameter // thread data );
创建线程Win32例子程序 #include “windows.h” DWORD WINAPI ThreadFunc(void pParam) { //…. Do some function return 0; } 线程的函数返回值,这个值在返回后,其他线程可以通过 GetExitCodeThread函数取得。
Int main() { hThreadHandle= CreateThread( NULL, //使用默认的安全属性 0, // 堆栈大小,使用windows默认(1M) ThreadFunc, // 线程的入口函数名 (LPVOID)NULL, // 线程入口函数的参数 0, // 表示线程立刻执行 &dwThreadID // 输出线程的ID ); dwRet= WaitForSingleObject(hThreadHandle, INFINITE ); // 等待线程退出 if ( dwRet== WAIT_OBJECT_0 ) { printf(“Work Thread Already Exits!\n”); } DeleteObject(hThreadHandle); // 释放内核对象资源 return 0; } 此时线程hThreadHandle的内核对象状态是未通知nonsignaled状态。 表示线程的内核对象已经变为已通知signaled状态。
创建线程MFC例子程序 • MFC封装了一个线程类CWinThread • 所有的MFC应用程序(对话框,单文档,多文档)中的主应用程序CWinApp都从CWinThread继承,即MFC应用程序的主线程在CWinApp中。 • MFC线程的创建方法 • AfxBeginThread全局函数 • AfxBeginThread全局函数可以创建一个CWinThread线程类,启动线程,并且返回CWinThread对象指针 • 得到CWinThread对象指针后可以控制线程的挂起(SuspendThread函数),恢复(ResumThread函数), 线程函数返回的时候,该对象被自动销毁。 • 直接声明CWinThread对象,并且调用它的方法CWinThread::CreateThread来创建线程。
创建线程AfxBeginThread If 0, the same priority as the creating thread will be used. For a full list and description of the available priorities, see SetThreadPriority in the Win32 Programmer’s Reference. • 工作线程形式的AfxBeginThread()的声明 CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc,// 线程入口函数 LPVOID pParam, // 线程入口函数参数 Int nPriority= THREAD_PRIORITY_NORMAL,// 线程优先级 UINT nStackSize= 0,// 堆栈大小 DWORD dwCreateFlags= 0,// 创建Flag,0表示创建后立刻运行 LPSECURITY_ATTRIBUTES lpSecurityAttrs= NULL//安全描述符 ); • 如果参数pfnThreadProc被指定,MFC认为这是一个工作线程,则用户指定的pfnThreadProc将被执行; • This function must be declared as follows: • UINT MyControllingFunction( LPVOID pParam ); • 若设为NULL,MFC假定它正在处理一个用户界面线程。于是调用该线程对象的InitInstance()函数进行线程的初始化--例如,建立主窗口和其它的用户界面对象。 • 如果InitInstance()成功返回(即返回TRUE),Run()函数被调用。CWinThread::Run()实现一个消息循环来处理所有该线程主窗口的消息
创建线程_AfxThreadEntry • AfxBeginThread函数创建一个CWinThread类,然后调用CWinThread::CreateThread函数创建线程 • CWinThread::CreateThread函数运行库_beginthreadex来启动线程,其入口函数是_AfxThreadEntry • _AfxThreadEntry的部分代码(参照MFC\SRC\THREADCORE.CPP) • if (pThread->m_pfnThreadProc!= NULL) • { • nResult= (*pThread->m_pfnThreadProc)(pThread->m_pThreadParams); • ASSERT_VALID(pThread); • } • // else --check for thread with message loop • else if (!pThread->InitInstance()) • { • ASSERT_VALID(pThread); • nResult= pThread->ExitInstance(); • } • else • { • // will stop after PostQuitMessagecalled • ASSERT_VALID(pThread); • nResult= pThread->Run(); • }
3.3 终止线程 • 线程中止的几种方法 • 线程的主入口函数返回 • 在线程内调用ExitThread,线程自行销毁 • 在同一个进程的其他线程中调用TerminateThread函数,终止了指定线程。 • 包含线程的进程中止运行
合理终止线程 • 线程的退出应该尽可能的使用第一种方式,让线程自己正常的退出。 • 始终都应该将线程设计成这样的形式,即,如果想要线程退出,则线程的函数就能够正常的返回。这是确保所有的线程资源被正确的清除的唯一办法。 • 对于线程的内核对象,总是在未通知的状态下创建的,当线程终止运行的时候,线程的内核对象的状态变为已通知 • 这样的特点可以使我们知道一个线程是何时终止运行的,我们只需要等待线程的内核对象从未通知变成已通知,则可以知道线程退出了。
线程正常终止的优点 • 在线程中创建和定义的C++对象能够自动的撤销 • 操作系统能够正确的释放线程的堆栈使用的内存 • 系统将线程的退出代码设置成为线程函数的返回值 • 系统将递减线程内核对象的使用计数 • 程序有机会正确的释放在线程中申请的各种资源
建议: • 1.如果该线程的处理函数中不进行任何等待的动作(WaitEvent,WaitSemaphore,Sleep等),则将占用大量CPU时间,造成用户界面线程反应慢。 • 2.对于除了大规模复杂科学计算的程序,用于处理某些事务和消息的任务通常在任务主循环中使用部分等待处理(WaitForSingleObject或者WaitForMutliObjects) • 例1:使用全局变量控制线程退出 • 一个工作线程ThreadFunc, 该线程进行某项复杂大规模计算,但是允许用户中断,在用户UI线程中根据按钮状态控制线程退出标志:g_bExitFlag; BOOL g_bExitFlag= TRUE; // 线程退出标志 DWORD ThreadFunc(PVOID pParam) { while(!g_bExitFlag) {// 通常对于g_bExtFlag的访问需要进行保护 // Here do function Function(Context); } return 0; }
例2:等待线程退出事件 HANDLE g_hExitEvent; DWORD ThreadFunc(PVOIDpParam) { BOOL bRunFlag=TRUE; while ( bRunFlag) { DWORD dwRet= WaitForSingleObject(g_hExitEvent, 500); if ( dwRet!= WAIT_OBJECT_0 ) {// 如果没等到退出Event,则进行TimeOut处理 // Here do function } else { // 等到了退出Event, (g_hExitEvent发生了状态变化) bRunFlag= FALSE; } } // 这里可以进行资源释放等善后工作。 return 0; } 用户想退出该工作线程,则可以调用SetEvent(g_hExitEvent),则g_hExitEvent变成已通知状态
等待线程终止 • 通常主线程应该监视创建的工作线程,主程序的退出也应该等所有的工作线程结束后,关闭工作线程内核对象,释放资源,然后退出 • 使用GetExitCodeThread来监视线程是否退出 • 例: • for(;;) • { • GetExitCodeThread(hThreadHandle,&ExitCode); • if ( ExitCode!= STILL_ACTIVE) break; • } • 缺点:占用了大量处理器资源,性能很差。 • 解决方法:可以适当增加睡眠:Sleep(200); • 使用WaitForSingleObject,来等待线程的对象句柄,监视线程的退出 • WaitForSingleObject(hThreadHandle, INFINITE); • 特点:当线程的主入口函数没有返回的话,该函数不会返回,等待的线程不会占用CPU.
支持线程处理函数Cancel • 需要花费较多时间进行事务处理的线程需要支持Cancel, 即用户希望不要再处理了. • 线程主函数必须能够”经常询问”Cancel状态 • 使用状态查询或者Cancel事件
线程在同一个地方进行等待 • 线程经常需要等待各种事件或者Message. • 如前面所说,线程可能等待退出Event或者某个事务Event. 也可能等待Message,Mail等 • 尽可能在代码的一个地方进行等待各种事件或者Message
DWORD WINAPI ThreadFunc(void pParam){ while(bRun){ dwRet= WaitForMultipleObjects( EVENT_MAX_NUM, g_ahEvtHandle, // event handle array FALSE, // 只要有一个事件触发,就可以返回 dwMilliseconds); // 超时值 if (WAIT_FAILED == dwRet) {// 错误处理} if ( WAIT_TIMEOUT==dwRet){//超时处理} switch ( dwRet-WAIT_OBJECT_0){ case EVENT_EXIT: bRun= false; // 做退出处理,结束循环,函数return case EVENT_EVT1: // 处理事件1 break; case EVENT_EVT2: // 处理事件2 break; default: // 默认处理 } } }
3.4 线程的暂停与回复 • 暂停线程的运行 • 线程内核对象中有一个值,用于指明线程的暂停次数。 • 可以在创建线程的时候,使这个线程处于暂停状态。 • DwCreationFlags参数可以指定为CREATE_SUSPENDED • 也可以调用SuspendThread挂起一个线程。 DWORD SuspendThread(HANDLE hThread); • 参数是需要挂起线程的句柄。 If the function succeeds, execution of the specified thread is suspended and the thread's suspend count is incremented. Suspending a thread causes the thread to stop executing user-mode (application) code. Each thread has a suspend count (with a maximum value of MAXIMUM_SUSPEND_COUNT). If the suspend count is greater than zero, the thread is suspended; otherwise, the thread is not suspended and is eligible for execution.
恢复线程的运行 • 要使线程成为可调度的线程,调用ResumeThread, 将线程的句柄传给他,则可以使线程成为活动的线程。 • ResumeThread • The ResumeThread function decrements a thread's suspend count. When the suspend count is decremented to zero, the execution of the thread is resumed. • DWORD ResumeThread( HANDLEhThread// handle to thread); • 使用SuspendThread必须小心。 • 因为不知道暂停一个线程的时候,它在干什么。如果线程正试图从堆中分配一块内存,那么,该线程将在该堆上设置一个锁。当其他线程试图访问该堆的时候,这些线程的访问就会被停止,直到第一个线程恢复运行。
线程睡眠 • 线程也能告诉系统,它不想在某个时间段内被调度。可以通过Sleep方式实现。 • void Sleep(DWORD dwMilliseconds); // 参数表示需要睡眠的时间,毫秒为单位。
线程的运行时间 • 可以使用GetThreadTimes来获得线程的运行时间等情况。 • Windows2000及其以上中才能用 • BOOL GetThreadTimes( HANDLE hThread, // 线程句柄 LPFILETIME lpCreationTime, // 线程创建时间 LPFILETIME lpExitTime, // 线程退出时间 LPFILETIME lpKernelTime, // 线程执行操作系统代码消耗了多少100ns 的CPU时间 LPFILETIME lpUserTime//线程执行用户代码消耗了多少100ns 的CPU时间 );
3.5 在类中使用线程 • 经常遇到这样的情况:希望在产生一个对象的同时也产生一个线程 • 线程的主入口函数或者使用类的静态函数或者使用全局函数,不能使用类的普通成员函数。 • 类成员的This指针可以联系线程和类
例: class CThreadOjb{ CThreadObj(); ~CThreadObj(); DWORD DoThreadFunc(); static DWORD WINAPI ThreadFunc(LPVOID param) { CThreadObj* pObj= (CThreadObj*)param;// 这里取得对象的指针 return pObj->DoThreadFunc();// 执行对象的处理函数 } void StartThread() { m_hThreadHandle=CreateThread(NULL,0, ThreadFunc, // 线程入口函数名 (LPVOID)this,// 传入对象的this指针作为参数 0,&ThreadID) }; //…其他成员函数和变量 } • ThreadFunc做成全局函数也具有同样的效果: