I/O完成端口、异步I/O、APC和线程池(四)——线程池

下面摘抄于MSDN《Thread Pooling》。
    有许多应用程序创建的线程花费了大量时间在睡眠状态来等待事件的发生。还有一些线程进入睡眠状态后定期被唤醒以轮询工作方式来改变或者更新状态信息。线程池可以让你更有效地使用线程,它为你的应用程序提供一个由系统管理的工作者线程池。至少会有一个线程来监听放到线程池的所有等待操作,当等待操作完成后,线程池中将会有一个工作者线程来执行相应的回调函数。
    你也可以把没有等待操作的工作项目放到线程池中,用QueueUserWorkItem函数来完成这个工作,把要执行的工作项目函数通过一个参数传递给线程池。工作项目被放到线程池中后,就不能再取消了。
    Timer-queue timers和Registered wait operations也使用线程池来实现。他们的回调函数也放在线程池中。你也可以用BindIOCompletionCallback函数来投递一个异步IO操作,在IO完成端口上,回调函数也是由线程池线程来执行。
    当第一次调用QueueUserWorkItem函数或者BindIOCompletionCallback函数的时候,线程池被自动创建,或者Timer-queue timers或者Registered wait operations放入回调函数的时候,线程池也可以被创建。线程池可以创建的线程数量不限,仅受限于可用的内存,每一个线程使用默认的初始堆栈大小,运行在默认的优先级上。
线程池中有两种类型的线程:IO线程和非IO线程。IO线程等待在可告警状态,工作项目作为APC放到IO线程中。如果你的工作项目需要线程执行在可警告状态,你应该将它放到IO线程。
    非IO工作者线程等待在IO完成端口上,使用非IO线程比IO线程效率更高,也就是说,只要有可能的话,尽量使用非IO线程。IO线程和非IO线程在异步IO操作没有完成之前都不会退出。然而,不要在非IO线程中发出需要很长时间才能完成的异步IO请求。
    正确使用线程池的方法是,工作项目函数以及它将会调用到的所有函数都必须是线程池安全的。安全的函数不应该假设线程是一次性线程的或者是永久线程。一般来说,应该避免使用线程本地存储和发出需要永久线程的异步IO调用,比如说RegNotifyChangeKeyValue函数。如果需要在永久线程中执行这样的函数的话,可以给QueueUserWorkItem传递一个选项WT_EXECUTEINPERSISTENTTHREAD。
    注意,线程池不能兼容COM的单线程套间(STA)模型。

    为了更深入地讲解操作系统实现的线程池的优越性,我们首先尝试着自己实现一个简单的线程池模型。
    代码如下:

typedef struct _THREAD_POOL
{
    HANDLE QuitEvent;
    HANDLE WorkItemSemaphore;

    LONG WorkItemCount;
    LIST_ENTRY WorkItemHeader;
    CRITICAL_SECTION WorkItemLock;

    LONG ThreadNum;
    HANDLE *ThreadsArray;

}THREAD_POOL, *PTHREAD_POOL;

typedef VOID (*WORK_ITEM_PROC)(PVOID Param);
typedef struct _WORK_ITEM
{
    LIST_ENTRY List;

    WORK_ITEM_PROC UserProc;
    PVOID UserParam;
    
}WORK_ITEM, *PWORK_ITEM;

DWORD WINAPI WorkerThread(PVOID pParam)
{
    PTHREAD_POOL pThreadPool = (PTHREAD_POOL)pParam;
    HANDLE Events[2];
    
    Events[0] = pThreadPool->QuitEvent;
    Events[1] = pThreadPool->WorkItemSemaphore;

    for(;;)
    {
        DWORD dwRet = WaitForMultipleObjects(2, Events, FALSE, INFINITE);

        if(dwRet == WAIT_OBJECT_0)
            break;

        // execute user's proc.

        else if(dwRet == WAIT_OBJECT_0 +1)
        {
            PWORK_ITEM pWorkItem;
            PLIST_ENTRY pList;

            EnterCriticalSection(&pThreadPool->WorkItemLock);
            _ASSERT(!IsListEmpty(&pThreadPool->WorkItemHeader));
            pList = RemoveHeadList(&pThreadPool->WorkItemHeader);
            LeaveCriticalSection(&pThreadPool->WorkItemLock);

            pWorkItem = CONTAINING_RECORD(pList, WORK_ITEM, List);
            pWorkItem->UserProc(pWorkItem->UserParam);

            InterlockedDecrement(&pThreadPool->WorkItemCount);
            free(pWorkItem);
        }

        else
        {
            _ASSERT(0);
            break;
        }
    }
    return 0;
}

BOOL InitializeThreadPool(PTHREAD_POOL pThreadPool, LONG ThreadNum)
{
    pThreadPool->QuitEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    pThreadPool->WorkItemSemaphore = CreateSemaphore(NULL, 0, 0x7FFFFFFF, NULL);
    pThreadPool->WorkItemCount = 0;
    InitializeListHead(&pThreadPool->WorkItemHeader);
    InitializeCriticalSection(&pThreadPool->WorkItemLock);
    pThreadPool->ThreadNum = ThreadNum;
    pThreadPool->ThreadsArray = (HANDLE*)malloc(sizeof(HANDLE) * ThreadNum);

    for(int i=0; i<ThreadNum; i++)
    {
        pThreadPool->ThreadsArray[i] = CreateThread(NULL, 0, WorkerThread, pThreadPool, 0, NULL);
    }

    return TRUE;
}

VOID DestroyThreadPool(PTHREAD_POOL pThreadPool)
{
    SetEvent(pThreadPool->QuitEvent);
    for(int i=0; i<pThreadPool->ThreadNum; i++)
    {
        WaitForSingleObject(pThreadPool->ThreadsArray[i], INFINITE);
        CloseHandle(pThreadPool->ThreadsArray[i]);
    }

    free(pThreadPool->ThreadsArray);

    CloseHandle(pThreadPool->QuitEvent);
    CloseHandle(pThreadPool->WorkItemSemaphore);
    DeleteCriticalSection(&pThreadPool->WorkItemLock);

    while(!IsListEmpty(&pThreadPool->WorkItemHeader))
    {
        PWORK_ITEM pWorkItem;
        PLIST_ENTRY pList;
        
        pList = RemoveHeadList(&pThreadPool->WorkItemHeader);
        pWorkItem = CONTAINING_RECORD(pList, WORK_ITEM, List);
        
        free(pWorkItem);
    }
}

BOOL PostWorkItem(PTHREAD_POOL pThreadPool, WORK_ITEM_PROC UserProc, PVOID UserParam)
{
    PWORK_ITEM pWorkItem = (PWORK_ITEM)malloc(sizeof(WORK_ITEM));
    if(pWorkItem == NULL)
        return FALSE;

    pWorkItem->UserProc = UserProc;
    pWorkItem->UserParam = UserParam;

    EnterCriticalSection(&pThreadPool->WorkItemLock);
    InsertTailList(&pThreadPool->WorkItemHeader, &pWorkItem->List);
    LeaveCriticalSection(&pThreadPool->WorkItemLock);

    InterlockedIncrement(&pThreadPool->WorkItemCount);
    ReleaseSemaphore(pThreadPool->WorkItemSemaphore, 1, NULL);
    return TRUE;
}

VOID UserProc1(PVOID dwParam)
{
    WorkItem(dwParam);
}

void TestSimpleThreadPool(BOOL bWaitMode, LONG ThreadNum)
{
    THREAD_POOL ThreadPool;    
    InitializeThreadPool(&ThreadPool, ThreadNum);
    
    CompleteEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 
    BeginTime = GetTickCount();
    ItemCount = 20;

    for(int i=0; i<20; i++)
    {
        PostWorkItem(&ThreadPool, UserProc1, (PVOID)bWaitMode);
    }
    
    WaitForSingleObject(CompleteEvent, INFINITE);
    CloseHandle(CompleteEvent);

    DestroyThreadPool(&ThreadPool);
}
    我们把工作项目放到一个队列中,用一个信号量通知线程池,线程池中任意一个线程取出工作项目来执行,执行完毕之后,线程返回线程池,继续等待新的工作项目。
    线程池中线程的数量是固定的,预先创建好的,永久的线程,直到销毁线程池的时候,这些线程才会被销毁。
    线程池中线程获得工作项目的机会是均等的,随机的,并没有特别的方式保证哪一个线程具有特殊的优先获得工作项目的机会。
    而且,同一时刻可以并发运行的线程数目没有任何限定。事实上,在我们的执行计算任务的演示代码中,所有的线程都并发执行。
    下面,我们再来看一下,完成同样的任务,系统提供的线程池是如何运作的。

DWORD BeginTime;
LONG  ItemCount;
HANDLE CompleteEvent;

int compute()
{
    srand(BeginTime);
    for(int i=0; i<20 *1000 * 1000; i++)
        rand();

    return rand();
}

DWORD WINAPI WorkItem(LPVOID lpParameter)
{
    BOOL bWaitMode = (BOOL)lpParameter;

    if(bWaitMode)
        Sleep(1000);
    else
        compute();

    if(InterlockedDecrement(&ItemCount) == 0)
    {
        printf("Time total %d second.\n", GetTickCount() - BeginTime);
        SetEvent(CompleteEvent);
    }
    return 0;
}

void TestWorkItem(BOOL bWaitMode, DWORD Flag)
{
    CompleteEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    BeginTime = GetTickCount();
    ItemCount = 20;
    
    for(int i=0; i<20; i++)
    {
        QueueUserWorkItem(WorkItem, (PVOID)bWaitMode, Flag);
    }    

    WaitForSingleObject(CompleteEvent, INFINITE);
    CloseHandle(CompleteEvent);
}
    很简单,是吧?我们仅需要关注于我们的回调函数即可。但是与我们的简单模拟来比,系统提供的线程池有着更多的优点。
    首先,线程池中线程的数目是动态调整的,其次,线程池利用IO完成端口的特性,它可以限制并发运行的线程数目,默认情况下,将会限制为CPU的数目,这可以减少线程切换。它挑选最近执行过的线程再次投入执行,从而避免了不必要的线程切换。
    系统提供的线程池背后的策略,我们下一节继续再谈。

参考书目

1,    MSDN Library 
2,    《Windows高级编程指南》
3,    《Windows核心编程》
4,    《Windows 2000 设备驱动程序设计指南》

什么是回调函数?

  简而言之,回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。

为什么要使用回调函数?

  因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。

  如果想知道回调函数在实际中有什么作用,先假设有这样一种情况,我们要编写一个库,它提供了某些排序算法的实现,如冒泡排序、快速排序、shell排序、shake排序等等,但为使库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,想让库可用于多种数据类型(int、float、string),此时,该怎么办呢?可以使用函数指针,并进行回调。

  回调可用于通知机制,例如,有时要在程序中设置一个计时器,每到一定时间,程序会得到相应的通知,但通知机制的实现者对我们的程序一无所知。而此时,就需有一个特定原型的函数指针,用这个指针来进行回调,来通知我们的程序事件已经发生。实际上,SetTimer() API使用了一个回调函数来通知计时器,而且,万一没有提供回调函数,它还会把一个消息发往程序的消息队列。

  另一个使用回调机制的API函数是EnumWindow(),它枚举屏幕上所有的顶层窗口,为每个窗口调用一个程序提供的函数,并传递窗口的处理程序。如果被调用者返回一个值,就继续进行迭代,否则,退出。EnumWindow()并不关心被调用者在何处,也不关心被调用者用它传递的处理程序做了什么,它只关心返回值,因为基于返回值,它将继续执行或退出。

  不管怎么说,回调函数是继续自C语言的,因而,在C++中,应只在与C代码建立接口,或与已有的回调接口打交道时,才使用回调函数。除了上述情况,在C++中应使用虚拟方法或函数符(functor),而不是回调函数。

原文地址:https://www.cnblogs.com/snailrun/p/2644861.html