windows 线程

在windows中进程只是一个容器,用于装载系统资源,它并不执行代码,它是系统资源分配的最小单元,而在进程中执行代码的是线程,线程是轻量级的进程,是代码执行的最小单位。 从系统的内核角度看,进程是一个内核对象,内核用这个对象来存储一些关于线程的信息,比如当前线程环境等等,从编程的角度看,线程就是一堆寄存器状态以及线程栈的一个结构体对象,本质上可以理解为一个函数调用,一般线程有一个代码的起始地址,系统需要执行线程,只需要将寄存器EIP指向这个代码的地址,那么CPU接下来就会自动的去执行这个线程,线程切换时也是修改EIP的值,那么CPU就回去执行另外的代码了。

什么时候不用多线程?

一般我们都讨论什么时候需要使用多线程,很多人认为只要计算任务能拆分成多个不想关的部分,就可以使用多线程来提升效率,但是需要知道的是,线程是很耗费资源的,同时CPU在线程之间切换也会消耗大量的时间,所以在使用上并不是多线程就是好的,需要慎重使用,在下列情况下不应该使用多线程: 1. 当一个计算任务是严重串行化的,也就是一个计算步骤严重依赖上一个计算步骤的时候。但是如果是针对不同的初始参数可以得到不同的结果那么可以考虑用多线程的方式,将每个传入参数当作一个线程,一次计算出多组数据。 2. 当多个任务有严格的先后逻辑关系的时候,这种情况下利用多线程需要额外考虑线程之间执行先后顺序的问题,实际上可能它的效率与普通的单线程程序差不多,它还需要额外考虑并发控制,这将得不偿失 3. 当一个服务器需要处理多个客户端连接的时候,优先考虑的是使用线程池,而不是简单的使用多线程,为每个客户端连接创建一个线程,这样会严重浪费资源,而且线程过的,CPU在进程调度时需要花大量的时间来执行轮询算法,会降低效率。

主线程

进程的入口函数就是主线程的入口函数,一般主线程推出进程退出(这是由于VC++中在主线程结束时会隐含的调用ExitProcess)

线程入口地址

在windows中需要为线程指定一个执行代码的开始地址,这个在VC++中体现为需要为每个进程制定一个函数指针,并制定一个void* 型的指针类型的参数。当CPU执行这个线程时会将寄存器环境改为这个线程指定的环境,然后执行代码(具体就是前面所说的更改EIP的值)

创建线程

在windows中创建线程所使用的API是CreateThread,这个函数的原型如下:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpsa, 
  DWORD cbStack, 
  LPTHREAD_START_ROUTINE lpStartAddr, 
  LPVOID lpvThreadParam, 
  DWORD fdwCreate, 
  LPDWORD lpIDThread
);

第一个参数是线程对象的安全描述符 第二个参数是线程栈的大小,每个线程都有一个栈环境用来存储局部变量,以及调用函数,这个值可以给0,这个时候系统会根据线程中调用函数情况动态的增长,但是如果需要很大的线程栈,比如要进行深层的递归,并且每个递归都有大量的局部变量和函数时,运行一段时间后,可能会出现没有足够大的地址空间再来增长线程栈,这个时候就会出现栈溢出的异常,为了解决这个问题,我们可以填入一个较大的值,以通知系统预留足够大的地址空间。 第三个参数是线程的入口函数地址,这个是一个函数指针,函数的原型是DWORD ThreadProc( LPVOID lpParameter); 函数中就是线程将要执行的代码。 第四个参数是函数中将要传入的参数,为了方便传入多个参数,一般将要使用的过个参数定义为一个结构体,将这个结构体指针传入,然后再函数中将指针转化为需要的结构体指针,这样就可以使用多个参数。 第五个参数是创建标志,默认一般传入0,但表示线程一被创建马上执行,如果传入CREATE_SUSPENDED,则表示线程先创建不执行,需要使用函数ResumeThread唤醒线程,另外在XP以上的系统中可以使用STACK_SIZE_PARAM_IS_A_RESERVATION结合上面的第二个参数,表示当前并不需要这么内存,只是先保留当前的虚拟地址空间,在需要时有程序员手动提交物理页面。如果没有指定那么回默认提交物理页面。 第六个参数会返回一个线程的ID。 下面是一个创建线程的例子:

typedef struct tag_THREAD_PARAM
{
    int nThreadNo;
    int nValue;
}THREAD_PARAM, *LPTHREAN_PARAM;

int _tmain(int argc, TCHAR *argv[])
{
    LPTHREAN_PARAM lpValues = (LPTHREAN_PARAM)HeapAlloc(GetProcessHeap(), 0, 10 * sizeof(THREAD_PARAM));
    for (int i =0; i < 10; i++)
    {
        lpValues[i].nThreadNo = i;
        lpValues[i].nValue = i + 100;
    }

    HANDLE hHandle[10] = {NULL};
    for(int i = 0; i < 10; i++)
    {
        hHandle[i] = CreateThread(NULL, 0, ThreadProc, &lpValues[i], 0, NULL);
        if (NULL == hHandle[i])
        {
            return 0;
        }
    }

    WaitForMultipleObjects(10, hHandle, TRUE, INFINITE);
    return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    LPTHREAN_PARAM pThreadValues = (LPTHREAN_PARAM)lpParam;
    printf("%d, values = %d\n", pThreadValues->nThreadNo, pThreadValues->nValue);
    return 0;
}

上述代码中我们将两个整型数据定义为了一个结构体,并在创建线程中,将这个结构体地址作为参数传入,这样在线程中就可以使用这样的两个参数了。

线程退出

当满足下列条件之一时线程就会退出 1. 调用ExitThread 2. 线程函数返回 3. 调用ExitProcess 4. 用线程句柄调用TerminateThread 5. 用进程句柄调用TerminateProcess 当线程终止时,线程对象的状态会变为有信号,线程状态码会由STILL_ACTIVE改为线程的退出码,可以用GetExitThreadCode得到推出码,然后根据推出码是否为STILL_ACTIVE判断线程是否在运行

线程栈溢出的恢复

使用C++时由于下标溢出而造成的栈溢出将无法恢复。当栈溢出时会抛出EXCEPTION_STACK_OVERFLOW的结构化异常,之后使用_resetstkflow函数可以恢复栈环境,这个函数只能在_except的语句块中使用。调用 SetThreadStackGuarantee函数可以保证当栈溢出时有足够的栈空间能使用结构话异常处理,SEH在底层仍然是使用栈进行异常的抛出和处理的,所以如果不保证预留一定的栈空间,可能在最后使用不了SEH。 下面是一个使用的例子:

void ArrayErr();
void Stackflow();
DWORD _exception_stack_overflow(DWORD dwErrorCcode);
int g_nCnt = 0;

int _tmain(int argc, TCHAR *argv[])
{
    for (int i = 0; i < 10; i++)
    {
        __try
        {
        /*  ArrayErr();*/
            Stackflow();
        }
        __except(_exception_stack_overflow(GetExceptionCode()))
        {
            int nResult = _resetstkoflw();
            if (!nResult)
            {
                printf("处理失败\n");
                break;
            }else
            {
                printf("处理成功\n");
            }
        }
    }
    return 0;
}

void ArrayErr()
{
    int array[] = {1, 2};
    array[10000] = 10;
}

void Stackflow()
{
    g_nCnt++;
    int array[1024] = {0};
    Stackflow();
}

DWORD _exception_stack_overflow(DWORD dwErrorCcode)
{
    if (EXCEPTION_STACK_OVERFLOW == dwErrorCcode)
    {
        return EXCEPTION_EXECUTE_HANDLER;
    }else
    {
        return EXCEPTION_CONTINUE_SEARCH;
    }
}

在上述的例子中定义两个会引起异常的函数,一个是下标越界,一个是深度的递归,两种情况都会引起栈溢出,但是下标越界是不可恢复的,所以这个异常不能被处理,在异常处理中我们使用函数_resetstkoflw来恢复栈,使得程序可以继续运行下去。

线程本地存储

当线程需要访问一个共同的全局变量,并且某个线程对这个变量的修改不会影响到其他的进程的时候可以给予每个线程一份拷贝,每个线程访问变量在它自己中的拷贝,而不用去争抢这一个全局变量,有时候我们可能会想出用数组的方式来存储这每份拷贝,每个线程访问数组中的固定元素,但是当线程是动态创建和销毁也就是线程数量动态变化时,维护这个数组将会非常困难,这个时候可以使用线程本地存储技术(TLS),它的基本思想是在访问全局变量时给每个线程一个实例,各个线程访问这个实例而不用去争抢一个全局变量。就好像系统为我们维护了一个动态数组,让每个线程拥有这个数组中的固定元素。使用TLS有两种方法关键字法和API法。 1. 关键字法: 使用关键字__declspec(thread)修饰一个变量,这样就可以为每个访问它的线程创建一份拷贝。 2. 动态API法,TlsAlloc为每个全局变量分配一个TLS索引, TlsSetValue为某个索引设置一个值,TlsGetValue获取某个索引的值TlsFree释放这个索引 两中方式各有优缺点,第一种方式使用简单,但是仅仅局限于VC++,第二中方式使用相对复杂但是可以跨语言只要能使用windowsAPI就可以使用这种方式 下面分别展示了使用这两种方式的例子:

DWORD _declspec(thread) g_dwCurrThreadID = 0;
DWORD WINAPI ThreadProc(LPVOID lpParam);

int main()
{
    g_dwCurrThreadID = GetCurrentThreadId();
    HANDLE hThread[10] = {NULL};
    for (int i = 0; i < 10; i++)
    {
        hThread[i] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
    }

    printf("Main Thread id = %x, g_dwCurrThreadID = %x, the address g_dwCurrThreadID = %08x\n", GetCurrentThreadId(), g_dwCurrThreadID, &g_dwCurrThreadID);
    WaitForMultipleObjects(10, hThread, TRUE, INFINITE);
    for (int i = 0; i < 10; i++)
    {
        CloseHandle(hThread[i]);
    }

    return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    g_dwCurrThreadID = GetCurrentThreadId();
    Sleep(100);
    printf("the thread id = %x, g_dwCurrThreadID = %x, the address g_dwCurrThreadID = %08x\n", GetCurrentThreadId(), g_dwCurrThreadID, &g_dwCurrThreadID);
    return 0;
}

首先定义了一个TLS的变量,然后在线程中引用,在输出结果中发现,没个线程中的值和它的地址值都不一样,所以说使用的只是一份拷贝而已

DWORD g_dwTLSIndex = 0;
DWORD WINAPI ThreadProc(LPVOID lpParam);

int main()
{
    HANDLE hThread[10] = {NULL};
    g_dwTLSIndex = TlsAlloc();
    TlsSetValue(g_dwTLSIndex, (LPVOID)GetCurrentThreadId());

    for (int i = 0; i < 10; i++)
    {
        hThread[i] = CreateThread(NULL, 0, ThreadProc, NULL, CREATE_SUSPENDED, NULL);
        ResumeThread(hThread[i]);
    }

    printf("Main Thread id = %x, g_dwCurrThreadID = %x\n", GetCurrentThreadId(), TlsGetValue(g_dwTLSIndex));
    WaitForMultipleObjects(10, hThread, TRUE, INFINITE);
    for (int i = 0; i < 10; i++)
    {
        CloseHandle(hThread[i]);
    }

    TlsFree(g_dwTLSIndex);

    return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    TlsSetValue(g_dwTLSIndex, (LPVOID)GetCurrentThreadId());
    printf("the thread id = %x, g_dwCurrThreadID = %x\n", GetCurrentThreadId(), TlsGetValue(g_dwTLSIndex));
    return 0;
}

上述使用中,在主线程中申请和释放一个TLS变量,在每个进程中仍然是使用这个变量,输出的结果也是每个变量都不同。

线程的挂起和恢复

用函数SuspendThread和ResumeThread控制线程的暂停和恢复,一个暂停的线程无法用ResumeThread来唤醒自身,除非有其他线程调用ResumeThread来唤醒。暂停的线程总是立即被暂停,而不管它执行到了哪个指令。 需要注意的是,线程这个内核对象中有一个暂停计数器,每当调用一个SuspendThread这个计数器会加一,而每当调用一次ResumeThread计数器会减一,只有当计数器为0时线程才会立即启动,所以可能会出现这样的情况,调用了ResumeThread之后线程并没有立即启动,这个是正常现象。 另外可以使用Sleep函数使线程休眠一段时间后再启动,这个填入的时间只是一个参考值,并不是填入多少,就让线程暂停多久,比如说我们填入10ms,这个时候当线程真正陷入到休眠状态时CPU可能执行其他线程去了,如果没有特殊情况,CPU会一直执行到这个线程的时间片结束。然后运行调度程序,调度下一个线程,所以说线程休眠的时间理论上最少也有20ms,通常会比我们设置的时间长。 在创建线程的时候可以使用CREATE_SUSPEND标志来明确表示以暂停的方式创建一个线程,如果不用这个方式,那么新创建线程的行为将难以掌握,有可能在CreateThread函数返回之后,线程就开始执行了,也有可能在返回前执行,所以推荐使用这个标志,创建完成后,进行想干的初始化操作,并在必要的时候调用ResumeThread启动它。

线程的寄存器状态

线程环境也就是线程在运行中,一大堆相关寄存器的值,这些值Windows维护在CONTEXT这个结构体中,在获取时可以通过设置结构体中成员的ContextFlag的值来表示我们需要获取哪些寄存器的值,一般填入CONTEXT_ALL或者CONTEXT_FULL获取所有寄存器的值,用SetThreadContext设置线程寄存器的值,用GetThreadContext获取线程的寄存器环境。需要注意的时,SetThreadContext只能用来修改通用寄存器的值,而像DS, CS这样的段寄存器是收到系统保护的,这些函数是处于RING3层,并不能进入到内核态。

线程调度的优先级

windows是抢占式多任务的,各个线程是抢占式的获取CPU,一般遵循先到先执行的顺序,windows中的带调度线程是存储在线程队列中的,但是这个队列并不是真正意义上的队列,这个队列是允许插队的,比如当用户点击了某个窗口,那么系统为了响应用户操作,系统会激活窗口并调整队列的顺序,将于窗口相关的线程排到较前的位置,这个插队时通过提高线程的优先级的方式实现的。我们在程序中可以使用SetThreadPriority调整线程的优先级。但是在程序中不要依赖这个值来判断线程的执行顺序,这个值对于系统来说只是一个参考值,当我们的线程进入到队列中时,系统会动态的调整它的优先级,如果某个进程由于优先级的问题长时间没有运行,系统可能会提高它的优先级,而有的线程由于优先级过高,已经执行了多次,系统可能会降低它的优先级。一来可以快速响应用户程序,二来可以防止某些恶意程序抢占式的使用CPU资源。

线程的亲缘性

线程的亲缘性与进程的相似,但是线程的亲缘性只能在进程亲缘性的子集之上,比如进程的亲缘性被设置在0 2 3 8这几个CPU上,线程就只能在这几个CPU上运行即使设置亲缘性为7,系统也不会将线程加载到这个7号CPU上运行。调用函数SetThreadAffinityMask可以设置亲缘性,使所有线程都在不同的CPU上运行,已达到真正的并发执行。下面是一个使用亲缘性的例子:

//设置CPU的位掩码
#define SETCPUMASK(i) (1<<(i))
DWORD WINAPI ThreadProc(LPVOID lpParam);
#define FOREVER for(;;);

int _tmain()
{
    SYSTEM_INFO si = {0};
    GetSystemInfo(&si);
    DWORD dwCPUCnt = si.dwNumberOfProcessors;

    for (int i = 0; i < dwCPUCnt; i++)
    {
        HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, CREATE_SUSPENDED, NULL);
        SetThreadAffinityMask(hThread, SETCPUMASK(i));
        ResumeThread(hThread);
        printf("启动线程%x成功", GetCurrentThreadId());
        system("PAUSE");
    }

    //线程创建成功,并绑定到各个CPU上,理论上CPU的利用率能达到100%,在任务管理器上查看性能可以看到CPU的利用率
    return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    //死循环
    FOREVER
}

上述程序首先判断了CPU核数,然后有几个CPU就创建几个线程,通过亲缘性设置为每个CPU绑定一个线程,在线程中我们什么都不做,只是一个死循环,主要是为了耗干CPU资源,这样通过查看资源管理器中的CPU使用情况发现CPU的使用率达到了100%,但是系统却没有一丝卡顿,这也说明Windows还是比较智能的,并没有让用户进程占据所有资源。

线程可警告状态与异步函数

在程序中可以通过一些方法使线程暂停,如使用SleepEx,Wait族的函数(是以Wait开始并且以Ex结尾的函数)可以使线程进入一种可警告状态,这种状态本质上是暂停当前线程,保存当前线程环境,并用这个环境加载并执行新的函数,当这个函数执行完成后,再恢复线程环境,继续执行线程接下来的代码。这些函数被称为异步函数。也就是说这些函数是借用当前的线程环境来作为自生的环境执行里面的代码。这样就类似于创建了一个轻量级的线程,它与线程的区别就是没有自身的环境状态,而是使用其他线程的环境状态,并且也不用进入到线程队列,供调度程序调度。 这些异步函数有自己的队列称为异步函数队列,当线程调用这些暂停或者等待的函数时,进入休眠状态,系统会保存当前线程环境,并从异步函数队列中加载异步函数,利用当前的线程环境继续运行,等到休眠时间到达后系统恢复之前保存的环境,继续执行线程的代码。 在使用时需要注意的是这些异步函数不要进行复杂的算法或者进程长时间的I/O操作,否则当线程休眠时间达到,而异步函数却未执行完,这样会造成一定的错误。 默认线程是不具有异步函数队列,可以利用函数QueueUserAPC将一个异步函数加入到队列中,然后利用上述所说的函数让线程进入假休眠状态,这样就会自动调用异步函数,下面是这样的一个例子:

VOID CALLBACK APCProc(ULONG_PTR dwParam)
{
    printf("%d Call APC Function!\n", dwParam);
}


int _tmain(int argc, TCHAR *argv[])
{
    for (int i = 0 ; i < 100; i++)
    {
        QueueUserAPC(APCProc, GetCurrentThread(), i);
    }

    //如果参数改为FALSE,或者注释掉这个函数,那么将不会调用这个APC函数
    SleepEx(10000, true);
    return 0;
}

上述代码中,我们在主线程中插入100个异步函数,虽然它们执行的都是同样的操作,然后让主线程休眠,SleepEx函数的第二个参数表示是否调用异步函数,如果填入FALSE,或者调用Sleep函数则不会调用异步函数。

线程的消息队列

线程默认是不具有消息队列的,同时也没有创建消息队列的函数,一般只需要调用与消息相关的函数,系统就会默认创建消息队列。 调用PostThreadMessage可以向指定的线程发送消息到消息队列,调用PostThread可以向当前线程发送消息到消息队列,在编写带有消息循环的线程时可能由于线程有耗时的初始化操作,发送到线程的消息可能还没有被接受就丢掉了,但是如果在进行初始化时调用消息函数,让其创建一个消息队列,可以解决这问题,下面是一段例子代码:

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    MSG msg = {0};
    /*利用消息相关的函数让线程创建消息队列,如果不创建,可能所有消息都不能收到
    这个时候子线程会陷入死循环,主线程会一直等待子进程*/
    PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE);
    //模拟进行耗时的初始化操作

    for (int i = 0; i < 10000000; i++);

    while (GetMessage(&msg, NULL, 0, 0))
    {
        printf("receive msg %d\n", msg.message);
    }

    printf("子线程退出\n");
    return 0;
}

int _tmain(int argc, TCHAR *argv[])
{
    DWORD dwThreadID = 0;
    HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadID);
    //让主线程休眠一段时间,一遍子线程能创建消息队列
    Sleep(10);
    //发送消息
    PostThreadMessage(dwThreadID, 0x0001, 0, 0);
    PostThreadMessage(dwThreadID, 0x0002, 0, 0);
    PostThreadMessage(dwThreadID, 0x0003, 0, 0);
    PostThreadMessage(dwThreadID, WM_QUIT, 0, 0);

    //休眠一段时间让子进程能够执行完成
    WaitForSingleObject(hThread, INFINITE);
    return 0;
}

线程执行时间

在一些性能分析工具中可能需要使用得到具体执行某一算法的函数的执行时间,一般调用GetTickCount计算调用前时间然后在算法函数调用完成后再次调用GetTickCount再次得到时间,这两个时间详相减则得到具体算法的时间,一般这种算法没有问题,但是需要考虑的时,如果在多任务环境下,该线程的时间片到达,CPU切换到其他线程,该线程没有运行但是时间却在增加,所以这种方式得到的结果并不准确,替代的方案是使用GetThreadTimes。 这个函数可以精确到100ns,并且得出系统执行该线程的具体时间,下面是函数的原型:

BOOL GetThreadTimes (
  HANDLE hThread, 
  LPFILETIME lpCreationTime, 
  LPFILETIME lpExitTime, 
  LPFILETIME lpKernelTime,
  LPFILETIME lpUserTime
);

这个函数后面几个输出参数分别是线程的创建时间,线程的推出时间,执行内核的时间以及执行用户代码的时间。下面演示了他的具体用法:

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    DWORD dwStart = GetTickCount();
    HANDLE hWait = (HANDLE)lpParam;
    //假设这是一个复杂的算法
    while (TRUE)
    {
        if (WAIT_OBJECT_0 == WaitForSingleObject(hWait, 0))
        {
            break;
        }
    }

    //模拟CPU调度其他线程
    Sleep(3000);
    DWORD dwEnd = GetTickCount();
    printf("子线程即将结束,当前执行算法所用的时间为:%d\n", dwEnd - dwStart);
    return 0;
}

int _tmain(int argc, TCHAR *argv[])
{
    HANDLE hWait = CreateEvent(NULL, TRUE, FALSE, NULL);
    DWORD dwCPUNum = 0;
    SYSTEM_INFO si = {0};
    GetSystemInfo(&si);
    dwCPUNum = si.dwNumberOfProcessors;
    HANDLE* phThread = (HANDLE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwCPUNum * sizeof(HANDLE));
    DWORD dwExitCode = 0;
    for (int i = 0; i < dwCPUNum; i++)
    {
        phThread[i] = CreateThread(NULL, 0, ThreadProc, hWait, CREATE_SUSPENDED, NULL);
        SetThreadAffinityMask(phThread[i], 1 << i);
        ResumeThread(phThread[i]);
    }

    _tsystem(_T("PAUSE"));

    //通知所有线程停止
    SetEvent(hWait);

    FILETIME tmCreate = {0};
    FILETIME tmExit = {0};
    FILETIME tmKernel = {0};
    FILETIME tmUser = {0};
    SYSTEMTIME sysTm = {0};
    ULARGE_INTEGER bigTmp1 = {0};
    ULARGE_INTEGER bigTmp2 = {0};

    for (int i = 0; i < dwCPUNum; i++)
    {
        GetExitCodeThread(phThread[i], &dwExitCode);
        printf("线程[H:0x%08X]退出,退出码:%u,以下为时间统计信息:\n", phThread[i], dwExitCode);
        GetThreadTimes(phThread[i], &tmCreate, &tmExit, &tmKernel, &tmUser);

        //得到创建时间
        FileTimeToLocalFileTime(&tmCreate, &tmCreate);
        FileTimeToSystemTime(&tmCreate, &sysTm);
        printf("\t创建时间:%2u:%02u:%02u.%04u\n", sysTm.wHour, sysTm.wMinute, sysTm.wSecond, sysTm.wMilliseconds);

        //得到退出时间
        FileTimeToLocalFileTime(&tmExit, &tmExit);
        FileTimeToSystemTime(&tmExit, &sysTm);
        printf("\t退出时间:%2u:%02u:%02u.%04u\n", sysTm.wHour, sysTm.wMinute, sysTm.wSecond, sysTm.wMilliseconds);

        //得到执行内核代码的时间
        FileTimeToLocalFileTime(&tmKernel, &tmKernel);
        FileTimeToSystemTime(&tmKernel, &sysTm);
        printf("\t退出时间:%2u:%02u:%02u.%04u\n", sysTm.wHour, sysTm.wMinute, sysTm.wSecond, sysTm.wMilliseconds);

        //得到执行用户代码的时间
        FileTimeToLocalFileTime(&tmUser, &tmUser);
        FileTimeToSystemTime(&tmUser, &sysTm);
        printf("\t退出时间:%2u:%02u:%02u.%04u\n", sysTm.wHour, sysTm.wMinute, sysTm.wSecond, sysTm.wMilliseconds);

        //线程开始时间
        bigTmp1.HighPart = tmCreate.dwHighDateTime;
        bigTmp1.LowPart = tmCreate.dwLowDateTime;
        bigTmp2.HighPart = tmExit.dwHighDateTime;
        bigTmp2.LowPart = tmExit.dwLowDateTime;

        //函数GetThreadTimes返回的时间单位是100ns,是微妙的10000倍
        printf("\t间隔时间(线程存活周期):%I64dms\n", (bigTmp2.QuadPart - bigTmp1.QuadPart) / 10000);

        //内核执行时间
        bigTmp1.HighPart = tmKernel.dwHighDateTime;
        bigTmp1.LowPart = tmKernel.dwLowDateTime;
        printf("\t内核模式(RING0)耗时:%I64dms!\n", bigTmp1.QuadPart / 10000);

        //执行用户程序的时间
        bigTmp2.HighPart = tmUser.dwHighDateTime;
        bigTmp2.LowPart = tmUser.dwLowDateTime;
        printf("\t内核模式(RING0)耗时:%I64dms!\n", bigTmp2.QuadPart / 10000);

        //实际占用总时间(用户代码时间 + 内核代码执行时间)
        printf("\t内核模式(RING0)耗时:%I64dms!\n", (bigTmp1.QuadPart  + bigTmp2.QuadPart)/ 10000);

        CloseHandle(phThread[i]);
        system("PAUSE");
    }

    //释放资源
    CloseHandle(hWait);
    HeapFree(GetProcessHeap(), 0, phThread);
    return 0;
}

以类成员函数的方式封装线程类

一般在如果要将线程函数封装到C++类中时一般采用的是静态成员的方式,因为C++中默认总会多传入一个参数this,而CreateThread需要传入的函数指针并不包含this,所以为了解决这个问题,一般传入一个静态函数的指针,但是静态函数不能定义为虚函数,也就是不能在派生类中重写它,所以在这介绍一种新的封装方式,将其封装为成员函数,并且允许被派生类重写。 它的基本思想:利用函数指针的强制转化让类成员函数指针强制转化为CreateThread需要的类型,这样在真正调用函数我们给定的函数地址时就不会传入this指针,但是为了使用类成员函数又需要这个指针,所以我们将this 指针的值变为参数,通过CreateThread的进行传递,这样就模拟了C++类成员函数的调用,下面是实现的部分代码:

//申明了这样一个线程的入口地址函数
DWORD WINAPI ThreadProc(LPVOID lpParam);
//创建线程的代码
DWORD CMyThread::CreateThread(LPSECURITY_ATTRIBUTES lpsa, DWORD cbStack, DWORD fdwCreate)
{
    typedef DWORD (WINAPI *LPTHREAD_START_ADDRESS)(LPVOID);
    typedef DWORD (WINAPI CMyThread::*PCLASS_PROC)(LPVOID);
    PCLASS_PROC pThreadProc = &CMyThread::ThreadProc;
    m_hThread = ::CreateThread(lpsa, cbStack, *(LPTHREAD_START_ADDRESS*)(&pThreadProc), this, fdwCreate, &m_dwThreadID);
    return (NULL != m_hThread) ? -1 : 0;
}

完整的代码 请戳这里下载

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏上善若水

L001 Linux和android ndk 外部程序调用popen 和system的用法

popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程。这个进程必须由 pclose() 函数关闭...

15820
来自专栏王亚昌的专栏

strace命令解析

strace常用于跟踪和分析进程执行时中系统调用和耗时以及占用cpu的比例,常用的格式如下:

14310
来自专栏安恒网络空间安全讲武堂

MS08-067漏洞调试分析详解

MS08-067漏洞调试分析详解 一、前言 在《Metasploit渗透测试魔鬼训练营》中有对MS08-067漏洞原理的分析,不过作者的文笔十分晦涩难懂,读起来...

374100
来自专栏ASP.NETCore

通过修改CoreCLR中的ClrHost实现自托管程序

上一篇我们讲了如何在windows和Linux上编译CoreClr的问题 虽然文章使用的是windows 10 (Bash)环境,但是也可以做为ubuntu环境...

16530
来自专栏一个爱瞎折腾的程序猿

MSMQ队列学习记录

使用MessageQueue类操作MSMQ,其在System.Messaging命名空间下,需要添加引用

17920
来自专栏喔家ArchiSelf

回顾Erlang简要

世界是并行的,Erlang程序反应了一种思考和交流的方式,个体通过发送消息进行交流,如果有个体死亡,其他个体会注意到。

12140
来自专栏大内老A

认识ASP.NET MVC的5种AuthorizationFilter

在总体介绍了筛选器及其提供机制(《深入探讨ASP.NET MVC的筛选器》)之后,我们按照执行的先后顺序对四种不同的筛选器进行单独介绍,首先来介绍最先执行的Au...

25460
来自专栏大内老A

Razor Engine,实现代码生成器的又一件利器

Razor Engine,之前仅仅是ASP.NET MVC的一种View引擎,目前已经完全成为一种可以独立使用的模版引擎,并且已经成为了CodePlex上一个开...

22990
来自专栏欧阳大哥的轮子

手把手教你查看和分析iOS的crash崩溃异常

一个应用程序并不总会一直运行的很好,它总会有出现crash崩溃的情况。如果在应用程序中接入了一些第三方的crash收集工具或者自建crash收集报告平台的话将会...

32620
来自专栏流柯技术学院

LoadRunner 调用Dll完成加密解密

C++里新建工程Class Library(此处是用VS2005,在VS6是:Win32 Dynamic-Link Library)

11940

扫码关注云+社区

领取腾讯云代金券