【多进程编程-线程】

Windows进程

进程的四种入口函数 
   int WINAPI WindMain()    |  WinMainCRTStartup
   int WINAPI wWinMain()    |  wWindMainCRTStartup
   int __cdecl main()        |  mainCRTStartup
   int __cdecl wMain()        |  wmainCRTStartup

操作系统不直接调用入口函数,
而是调用c/c++运行库的启动函数 

操作系统启动进程

检索指向新进程的完整命令行的指针 
检索指向新进程的环境变量的指针 
对C/C++运行期的全局变量初始化 
对C 运行期内存单元分配函数(malloc 和 calloc )和其他的底层输出输入例程使用的内存堆进行初始化 
为所有全局和静态的C++对象调用构造函数 

线程的构成

线程的内核对象, 操作系统用它来对线程实施管理 
线程堆栈,用于维护线程在执行代码时候需要的所有函数局部变量。
线程上下文(一组CPU寄存器状态,特别是指令指针寄存器和堆栈指针寄存器)
指令寄存器和堆栈寄存器记录的地址都用于标志拥有线程的进程地址空间中的内存.

线程和进程

进程比线程使用更多的系统资源,原因是它需要更多的地址空间。为进程创建一个虚拟地址空间,需要很多系统资源,同时,系统中要保留大量的记录,这也要占用大量的内存。
另外,dll 或者 exe需要加载到一个地址空间,也需要文件资源。
线程只需要一个内核对象和一个堆栈,保留的记录很少,因此需要很少的内存 
Windows中进程是不活泼的,进程从来不执行任何东西,进程只是线程的容器  
应该用增加线程来解决编程问题,避免创建新的进程。

Windows线程的调度 

抢占式操作系统必须使用某种算法来确定哪些线程应该在何时调度和运行多长时间 
Windows被成为抢占式多线程操作系统,因为一个线程可以随时停止运行,然后另外一个线程进行调度。 
基于任务优先级的抢占式调度算法,同一优先级的任务遵循时间片轮转,并且遵循FIFO策略。

Windows线程的调度

每隔20ms左右,windows要查看当前所有的线程内核对象,在这些内核对象中,只有某些对象被视为可以调度的对象,Windows选择可调度线程内核对象中的一个,将他加载到CPU寄存器中,然后继续运行,当系统引导时,便可以加载CPU寄存器中的线程上下文,使线程运行。 
系统只调度可以调度的线程。实际情况是很多线程是不可调度的线程,比如一个暂停运行的线程(可以在创建线程的时候,直接指定这个线程是暂停的。)比如一个正在等待某些事情发生的线程。 
CPU不给无事可作的线程分配CPU时间。 

Windows线程的优先级

每个线程都会被赋予一个从0-31的优先级号码
只要是高优先级的线程是可以调度的,系统绝对不会调度低优先级的(渴求调度Starvation)
系统引导的时候创建特殊的线程——0页线程,其优先级为0,当系统中没有任何其他线程运行时,0页线程负责将系统中所有的空闲RAM页面置0
进程内使用线程相对的优先级:空闲、最低、低于正常、正常、高于正常、最高、关键时间
进程也根据具体情况被分为7个进程优先级类:空闲、低于正常、正常、高于正常、实时
线程的实际优先级是进程优先级类和进程内线程相对优先级的组合
正常优先级类的进程的基本优先级是5, 进程内正常优先级线程的优先级是8, 所以一个正常进程中的正常优先级线程的真正优先级是13

何时创建一个进程的主线程 

线程用于描述进程中的运行路径。 每当进程被初始化,系统就要创建一个主线程。 该线程和C/C++运行库的启动代码一道开始运行,启动代码则调用进入点函数。并且继续运行直到进行点函数返回并且C/C++运行库的启动代码调用ExitProcess为止 

线程进入点函数 

每个线程必须有一个进入点函数,线程从这个进入点开始运行。
一个进程的主线程的进入点函数
        main, wmain, WinMain, wWinMain.
一个辅助线程的进入点函数:例如
DWORD WINAPI ThreadFunc(
                            PVOID pvParam) 
{
        DWORD dwResult = 0;
            …..
        return dwResult.
}

创建线程 - Windows

CreateThread函数。在一个已经运行的线程中创建辅助线程
HANDLE CreateThread(
    LPSECURITY_ATTRIBUTES lpThreadAttributes,     
            (线程的安全属性 建议NULL )
    DWORD dwStackSize,             
            (线程的堆栈大小,建议0)
    LPTHREAD_START_ROUTINE lpStartAddress,     
            线程入口函数地址
    LPVOID lpParameter,             
            线程参数
    DWORD dwCreationFlags,             
            设定控制创建线程的其他标志。
    LPDWORD lpThreadId);
线程ID. 线程ID是操作系统赋予一个线程的标志,在整个系统中是唯一的。 

创建线程 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
    // 此时线程hThreadHandle的内核对象状态是 未通知 状态。
    dwRet = WaitForSingleObject(hThreadHandle, INFINITE ); // 等待线程退出
    if ( dwRet == WAIT_OBJECT_0 ) {    // 表示线程的内核对象已经变为 已通知 状态。
        printf(“Work Thread Already Exits!\n”);
    }
    DeleteObject(hThreadHandle);    // 释放内核对象资源
    return 0;    
}

创建线程 MFC例子程序

MFC封装了一个线程类CWinThread
所有的MFC应用程序(对话框,单文档,多文档)中的主应用程序CWinApp都从CWinThread继承,即MFC应用程序的主线程在CWinApp中。
AfxBeginThread全局函数可以创建一个CWinThread线程类,启动线程,并且返回CWinThread对象指针
得到CWinThread对象指针后可以控制线程的挂起(SuspendThread函数),恢复(ResumThread函数), 线程函数返回的时候,该对象被自动销毁。
也可以直接声明CWinThread 对象,并且调用它的方法CWinThread::CreateThread来创建线程。

创建线程 AfxBeginThread

工作线程形式的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//安全描述符);

当AfxBeginThread函数的参数pfnThreadProc被指定的话,MFC认为这是一个工作线程,则用户指定的pfnThreadProc将被执行

当pfnThreadProc是NULL的时候,MFC假定它正在处理一个用户界面线程。于是调用该线程对象的InitInstance()函数进行线程的初始化--例如,建立主窗口和其它的用户界面对象。如果InitInstance()成功返回(即返回TRUE),Run()函数被调用。CWinThread::Run()实现一个消息循环来处理有该线程主窗口的消息 

工作线程的入口函数写法: 
    UINT ThreadFunctionFunction(LPVOID pParam);

创建线程 _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 PostQuitMessage called
    ASSERT_VALID(pThread);
    nResult = pThread->Run();
}

终止线程 

线程中止的几种方法:
线程的主入口函数返回 
在线程内调用ExitThread,线程自行销毁
在同一个进程的其他线程中调用TerminateThread函数,终了指定线程。
包含线程的进程中止运行  

合理终止线程

线程的退出应该尽可能的使用第一种方式,让线程自己正常的退出。
始终都应该将线程设计成这样的形式,即,如果想要线程退出,则线程的函数就能够正常的返回。这是确保所有的线程资源被正确的清楚的唯一办法。 
对于线程的内核对象,总是在未通知的状态下创建的,当线程终止运行的时候,线程的内核对象的状态变为已通知 
这样的特点可以使我们知道一个线程是何时终止运行的,我们只需要等待线程的内核对象从未通知变成已通知,则可以知道线程退出了。

线程正常终止的优点

在线程中创建和定义的C++对象能够自动的撤销 
操作系统能够正确的释放线程的堆栈使用的内存 
系统将线程的退出代码设置成为线程函数的返回值 
系统将递减线程内核对象的使用计数 
程序有机会正确的释放在线程中申请的各种资源

线程终了 例子程序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;
}
建议:
如果该线程的处理函数中不进行任何等待的动作(WaitEvent,WaitSemaphore,Sleep等),则将占用大量CPU时间,造成用户界面线,程反应慢。
对于除了大规模复杂科学计算的程序,用于处理某些事务和消息的任务通常在任务主循环中使用部分等待处理(WaitForSingleObject或者WaitForMutliObjects)

线程终了 例子程序2

例子二    等待线程退出事件
HANDLE g_hExitEvent;
DWORD ThreadFunc(PVOID pParam){
    BOOL bRunFlag = TRUE;
    while ( bRunFlag ){
        DWORD dwRet = WaitForSingleObject(g_hExitEvent, 500);
        // 使用500毫秒的等待,如果g_hExitEvent是已通知状态,则
        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, 即用户希望不要再处理了.
典型的例子:Navi中用户进行某项内容的检索(比如检索全国火车站), 可能需要耗费较多时间, 此时需要支持用户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: // 默认处理
        }
    }
}

暂停线程的运行 

线程内核对象中有一个值,用于指明线程的暂停次数。
可以在创建线程的时候,使这个线程处于暂停状态。DwCreationFlags 参数可以指定为CREATE_SUSPENDED 
也可以调用SuspendThread 挂起一个线程。
DWORD SuspendThread(HANDLE hThread); 参数是 需要挂起线程的句柄。 

恢复线程的运行 

要使线程成为可调度的线程,调用ResumeThread, 将线程的句柄传给他,则,可以使线程成为活动的线程。
单个线程可以暂停若干次,如果一个线程暂停了三次,它必须被恢复三次,然后它才可以被分配给一个CPU。 
使用SuspendThread必须小心。因为不知道暂停一个线程的时候,它在干什么。如果线程正试图从堆中分配一块内存,那么,该线程将在该堆上设置一个锁。当其他线程试图访问该堆的时候,这些线程的访问就会被停止,直到第一个线程恢复运行。 

睡眠方式 

线程也能告诉系统,它不想在某个时间段内被调度。可以通过Sleep方式实现。
void Sleep(DWORD dwMilliseconds);
 // 参数表示需要睡眠的时间,毫秒为单位。 

线程的运行时间 

可以使用GetThreadTimes来获得线程的运行时间等情况。(Windows2000中才能用)
BOOL GetThreadTimes (
HANDLE hThread,             // 线程句柄
LPFILETIME lpCreationTime,     // 线程创建时间
LPFILETIME lpExitTime,         // 线程退出时间
LPFILETIME lpKernelTime,    // 线程执行操作系统代码消耗了多少 100ns 的CPU时间
LPFILETIME lpUserTime);        // 线程执行用户代码消耗了多少 100ns 的CPU时间 

在类中使用线程

经常遇到这样的情况:希望在产生一个对象的同时也产生一个线程
线程的主入口函数或者使用类的静态函数或者使用全局函数,不能使用类的普通成员函数。
类成员的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做成全局函数也具有同样的效果:

Tron-线程创建和激活

创建任务
创建主要通过cre_tsk调用来实现的。
激活任务
Itron系统中,初始创建的任务的状态是Dormant状态,这个时候任务还处于系统无法调度的状态,所以必须激活任务,激活任务的系统调用是sta_tsk,作用就是将指定任务由Dormant状态迁移到Ready状态。

Tron-线程终止

终止任务
    在Itron系统中如果需要实现将任务由ready state, run state, wait state, suspend state, wait-suspend state迁移到Dormant状态的话,需要通过下面两种方式实现,正常终止和强制终止。
    正常终止是任务自身主动放弃系统的使用权。
    强制终止是任务自身出现错误,无法自主释放系统使用权,这个时候只能通过其他任务来完成对本任务使用权的剥夺。
实现任务终止的系统调用主要有下面三个:
ext_tsk system call
    任务从Run状态切换到dormant 状态.(在自身任务中调用)
exd_tsk system call
    任务从Run状态切换到non-existent  状态(在自身任务中调用).
ter_tsk system call
    终了指定线程, 使被终了的线程处于dormant state.

Tron-线程销毁

实现任务状态从Run或者Dormant状态切换到nonexistent状态,实现这个功能主要通过下面两个调用来完成的。
exd_tsk system call
    The task that issued the exd_tsk system call is switched from the run state to the non-existent state.
del_tsk system call
    The task specified by the parameters is switched from the dormant state to the non-existent state 
 

你可能感兴趣的:(经验分享)