基于Windows APC写一个简单的多线程并发库

Windows APC

首先我们要知道什么是APCAPC的全称是Asynchronous Procedure Call,异步过程调用。MSDN链接请猛击这里。用简短的话来总结就是

  • 每一个线程都有一个APC队列
  • APC分两种,分别是kernel-mode APC和user-mode APC
  • 通过调用QueueUserAPC可以向user-mode APC队列中加入APC对象
  • 每一个APC对象都关联一个APCProc签名形式的回调函数
  • 当线程处于alterable状态时,当前的线程就会去执行APC队列里的回调函数
  • 有6个函数可以让当前的线程进入alterable状态,他们是SleepEx,WaitForSingleObjectEx,WaitForMultipleObjectEx,SignalObjectAndWait,GetQueuedCompletionStatusEx和MsgWaitForMultipleObjectsEx
  • 上述函数的非Ex(如果有的话)版本,将重置线程的alterable状态

了解这些内容后,我们可以发现,APC提供了一个实现线程池的可行方案。我们只要能将某个线程一直处于alterable状态,然后将线程的执行体封装在APCProc中,通过调用QueueUserAPC就可以复用当前的线程,避免频繁创建和销毁线程。

那我们来看如何写利用APC来写一个多线程并发库。首先来看怎么设计线程类。

thread类

我们知道在Win32下,创建一个线程只要调用CreateThread并附带回调函数函数ThreadProc就可以了。那么我们的thread类大致的样子就是:

class thread
{
public:
    thread_imp(void);
    ~thread_imp(void);

private:
    static DWORD WINAPI ThreadProc(LPVOID);

    HANDLE _thread_native_handle;
    DWORD  _thread_id;
};

要让创建出来的线程能被反复利用,就需要让该线程一直处于alterable状态,因此需要调用前面说到的6个函数中的一个。这里我选用WaitForSingleObjectEx,那么ThreadProc就看上去像这样子:

DWORD WINAPI thread::ThreadProc(LPVOID param)
{
    thread* this_thread = static_cast<thread*>(param);
    assert(this_thread);

    while (true)
    {
        If (WAIT_OBJECT_0 == WaitForSingleObjectEx(_thread_stop_event, INFINIT, TRUE))
        {
            return 0;
        }
    }
    return 0;
}

其中为了配合WaitForSigleObjectEx,我们还需要定义一个event对象,HANDLE _thread_stop_event。那么thread类的构造和析构函数就像这个样子了:

thread::thread(void)
    : _thread_stop_event(::CreateEvent(NULL, TRUE, FALSE, NULL))
    , _thread_native_handle(0), _thread_id(0)
{
    _thread_native_handle = ::CreateThread(NULL, 0, ThreadProc, this, 0, &_thread_id);
}

thread::~thread(void)
{
    ::SetEvent(_thread_stop_event);
    if (WaitForSingleObject(_thread_native_handle, INFINIT))
    {
        ::CloseHandle(_thread_native_handle);
    }
    ::CloseHandle(_thread_stop_event);
}

到此我们创建一个thread对象实例就得到了一个一直处于alterable状态的线程了。剩下的事情就是利用QueueUserAPC函数来复用刚才的线程。根据MSDN,我们只要定义一个APCProc,然后创建的这个线程就可以被我们复用了。

为了让我们的thread类在处理APCProc回调函数上更加通用以及在使用上更和谐统一,我们设想线程在使用上是处理一个具体的任务而不是一个简单的过程。因此我们封装一下QueueUserAPC,把它变成thread的一个成员函数,thread::QueueTask(LPVOID)。LPVOID类型参数包含了这个线程要执行的任务的全部信息。我们可以把这种信息定义为线程的上下文,thread_context。因此我们直接将LPVOID类型替换成thread_context类型。

BOOL thread::QueueTask(thread_context* context)
{
    return static_cast<BOOL>(::QueueUserAPC(APCProc, _thread_native_handle, reinterpret_cast<ULONG_PTR>(context)));
}

APCProc该如何处理context就看你自己怎么定义了。

线程类定义完了后,我还需要一个类来协调我们和thread类之间的交互。我把这个负责交互的类定义为threadpool。

threadpool类

threadpool类所要做的事情还是比较明确的:

  1. 创建出一定数量的线程
  2. 将一个具体的任务分配给某个线程来执行

由此我们可以想象threadpool类的样子可能是这样的:

class threadpool
{
public:
    threadpool(LONG_PTR thread_count);
    ~threadpool();

    BOOL QueueTask(LPVOID task);

private:
    LONG_PTR _thread_count;
    thread*  _threads;
};

其中的构造函数就是根据thread_count创建出threads数组。QueueTask就是找到一个线程对象,然后调用thread::QueueTask。至于threadpool::QueueTask中LPVOID具体是什么类型,就看自己怎么想了。

结束语

基本上,我把如何利用APC来构建一个多线程的并发库介绍完了。其中的一些细节这里做了隐藏,有兴趣的同学可以自己尝试下。我大概写了一个样板程序,可以猛击这里查看。

原文地址:https://www.cnblogs.com/wpcockroach/p/3084681.html