65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 异步设计模式在原生 C++ 中的应用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (14投票s)

2009年8月24日

GPL3

18分钟阅读

viewsIcon

46413

downloadIcon

820

使用 Windows I/O 完成端口实现 .NET 异步设计模式的原生 C++ 代码

,

AsyncInvokeDemo1.png

目录

引言

Code Project 为社区带来了大量资源,包括我在内的许多人都从中受益。既然这个网站对我帮助很大,我想是时候通过发表一篇自己的文章来回馈社区了。我一直想发帖,但从未找到(或者说,抽出)时间。好了,这是我的第一篇文章;请随时提供您的反馈和问题,我将尽力回答。

.NET Framework 大大改进了对多线程应用程序的支持,从而使程序员更容易创建此类应用程序。尽管 .NET Framework 提供了大量的工具和资源来完成各种任务,但仍有许多程序员使用 WTL/ATL/MFC 甚至传统的 Win32 API 等旧库来完成他们的任务。

对于某些任务,我属于后一组程序员,有时发现从使用 .NET Framework 切换过来既困难又耗时耗力。出于这个原因,在 C++0x 为您附近的编译器带来多线程支持之前,我想在 C++ 中重新创建我最常使用的 .NET Framework 功能之一:异步设计模式,特别是使用委托异步调用函数(例如,在线程池上)并稍后检索其结果的能力。

.NET Framework 异步设计模式还具有一个用于在特定线程上同步函数调用的接口(ISynchronizeInvoke),我也将其包含在此实现中。

目标

此实现的主要目标是为 C++ 语言带来 .NET 异步设计模式的一些非常基本的实现。由于我主要在开发原生 Win32 应用程序时使用 WTL/ATL,因此我使此实现与它们各自的风格良好融合。此实现绝不提供所有答案,而应仅被视为未来想法和增强的起点和学习项目。

考虑到依赖关系有时很难管理,并且考虑到此项目的大小,我将实现放在一个头文件中,使其非常容易包含在现有项目中。如果我不担心额外的依赖关系,我可以使用出色的 Boost C++ 库来受益于它们对函数对象、元组和线程的扩展(和可移植)支持,但是对于这个小项目,我决定从精简的依赖关系开始。此外,由于其命名约定和大量使用遵循 STL 指南的模板,Boost 不会与 WTL/ATL 风格很好地融合。此实现强制的唯一库依赖关系是使用 ATL(特别是 CThreadPool),这可以通过重新实现 CDelegate 类以使用自定义线程池(或任何其他执行委托的方法)来轻松删除。

您还应该注意,.NET Framework 受益于 CLR 功能,因此强制我的实现使用不同的技术和机制来模拟某些行为。此外,某些功能尚未实现(例如,对多播委托、可变参数等的支持)。

更具体地说,此实现提供以下内容

  • 异步设计模式部分示例实现的文档化源代码
  • 能够异步调用全局或类函数并在稍后检索其结果
  • 支持在委托异步调用完成时进行回调
  • 一种检测是否需要异步调用的方法以及在特定线程上同步该调用的能力
  • 委托和异步资源的自动内存管理

并且不提供

  • 线程安全
  • 委托参数类型安全
  • 可变参数(此实现具有预定义的委托签名)
  • 多播委托
  • 异常支持
  • 错误处理

背景

要更好地理解本文,您应该熟悉异步设计模式。Code Project 上有很多关于它们的优秀文章,以下是一些引起我注意的文章:

此外,.NET 异步设计模式在 MSDN 网站上有完整的文档

异步编程设计模式
http://msdn.microsoft.com/en-ca/library/ms228969.aspx

由于此实现使用 Windows I/O 完成端口,您还应该考虑了解它们的工作原理。您将在 MSDN 上找到相关文档

I/O 完成端口 (Windows)
http://msdn.microsoft.com/en-us/library/aa365198(VS.85).aspx

此项目还使用了 ATL Worker Archetype 通过 CThreadPool 类异步执行委托,这也在 MSDN 上有文档记录

ATL 服务器库参考 - Worker Archetype
http://msdn.microsoft.com/en-us/library/ytkt93h8(VS.80).aspx

建议的实现

此实现的核心依赖于 IAsyncResultISynchronizeInvokeIDelegateIDelegate 在 .NET 中不存在,但我需要一个接口来定义委托,这在 C++ 中不存在(而且函数指针比委托的级别低得多)。我以 .NET Framework 为指导实现了 IAsyncResult(它封装了异步调用的所有资源),并创建了两个类 CAsyncResultCThreadMethodEntry,分别供 CDelegateCSynchronizeInvoke 使用。

让我们继续使用类图(一张图片胜过千言万语)

ClassDiagram1.png

注意:CMainDlg 是演示应用程序的一部分,而不是实现的一部分。

该图应该是不言自明的,设计遵循了类似 .NET 类的一些语义,但这里有一些更详细的信息

  • 虽然 CAsyncRequestIDelegateIClassDelegate 可以在应用程序中使用(IDelegate 比其他两个更多),但它们被设计为在此实现中内部使用。
  • IAsyncResult 用于保留对异步调用的引用。保留此异步调用引用并使用它调用 EndInvoke 非常重要,否则将导致内存泄漏,除非将其作为“即发即弃”类型的调用(有关此主题的更多详细信息如下)。
  • 重要的是要提到 IDelegateISynchronizeInvoke 密切相关,并且都具有 BeginInvokeEndInvoke 方法,但它们的含义非常不同。

    IDelegate::BeginInvoke 用于开始委托包装的函数的异步调用,该委托实现了 IDelegate 接口,而 ISynchronizeInvoke::BeginInvoke 旨在在实现 ISynchronizeInvoke 接口的对象所在的线程上(或它决定调用的任何线程上)异步调用提供的委托。因此,传递给 IDelegate::BeginInvoke 的委托是回调,而传递给 ISynchronizeInvoke::BeginInvoke 的委托是要在实现 ISynchronizeInvoke 的对象选择的线程上调用的委托。
  • IAsyncResult 遵循与其 .NET 对应项相同的语义,只是它缺少 CompletedSynchronously getter,这在此实现中没有用。
  • CAsyncResultCThreadMethodEntry 分别用于封装与 CDelegateCSynchronizeInvoke 的异步调用相关的资源管理。CAsyncRequest 只是一个抽象类,实现了 CAsyncResultCThreadMethodEntry 的共同部分。

调用 EndInvoke() 是强制性的吗?

在进一步讨论接口和实现之前,我想解决是否需要调用 EndInvoke() 的问题。在 .NET 实现中,如果您不对委托异步调用调用 EndInvoke(),则会声明您将泄漏资源(MSDN 上有文档记录),但如果您不对同步调用调用 EndInvoke()(如 WinForms 中使用的),显然不会发生泄漏(未正式记录)。

由于接口文档声明应始终调用 EndInvoke(),因此实现者有责任正式记录接口规则的任何例外情况。由于没有官方文档说明 EndInvoke() 在 WinForms 实现中是可选的,因此可以假定它是必需的。(这仍然可以讨论,因为 WinForms 实现并没有真正遵循模式的接口定义。)

为了解决这个问题,我修改了接口定义,包含一个额外的参数来声明调用是“即发即弃”类型还是将调用 EndInvoke() 来检索返回值(并释放分配的资源)。为了遵循与 .NET 实现的一些相似性,我为同步调用包含了一个默认值为“即发即弃”,而对于委托异步调用则不是“即发即弃”。

以下是此实现中“即发即弃”场景的规则

  • 对于非“即发即弃”调用,始终对委托异步和同步调用调用 EndInvoke(),否则将泄漏资源。
  • 对于“即发即弃”调用,切勿对委托异步或同步调用调用 EndInvoke(),因为与调用相关的资源已经处置。

接口详情

以下是核心功能的签名和参数详情

IDelegate::BeginInvoke
IAsyncResult *BeginInvoke(ULONG_PTR ulParam = 0, IDelegate *pCallback = NULL, LPVOID pvState = NULL, BOOL bFireAndForget = FALSE)

  • ulParam - 委托参数;这将传递给委托类包装的函数
  • pCallback - 回调委托;一个可选委托,它包装一个在异步调用完成后调用的函数。为了简化回调的使用(与委托相关的任何资源),它们在完成后会自动删除。(始终使用提供的委托宏来创建新委托。详见下文)
  • pvState - 与异步调用相关的可选状态;可以是您想保留在上下文中的任何内容
  • bFireAndForget - 此调用是否是“即发即弃”场景。

ISynchronizeInvoke::BeginInvoke
IAsyncResult *BeginInvoke(IDelegate *pDelegate, ULONG_PTR ulParam = 0, BOOL bFireAndForget = TRUE)

  • pDelegate - 要调用的委托包装函数;此调用将在实现 ISynchronizeInvoke 的对象决定进行的线程上进行,通常是实现接口的对象所在的线程;对于 .NET Framework 实现,是 GUI 线程
  • ulParam - 委托参数,传递给 pDelegate 包装的函数
  • bFireAndForget - 此调用是否是“即发即弃”场景。

IDelegate::EndInvokeISynchronizeInvoke::EndInvoke
ULONG_PTR EndInvoke(IAsyncResult **ppAsyncResult)

  • ppAsyncResult - 要结束的异步调用的引用指针;这已实现为双指针,因为结果指针设置为 NULL,以指示在调用 EndInvoke 后绝不能使用 IAsyncResult 引用指针(因为所有相关资源都已解除分配)。
  • EndInvoke 的返回值是委托包装函数的返回值。客户端必须使用类型转换来恢复原始类型。
  • EndInvoke 对于 IDelegateISynchronizeInvoke 具有相同的签名。要检索异步调用的结果,必须调用 EndInvoke。如上所述,除非 BeginInvoke 作为“即发即弃”类型的调用,否则必须始终调用 EndInvoke 来回收为异步调用分配的资源。如果在异步调用完成之前调用 EndInvoke,它将阻塞直到调用完成。还值得一提的是,回调永远不是异步调用时间框架的一部分;也就是说,调用 EndInvoke 不会阻塞直到回调完成。这很重要,因为否则在回调中调用 EndInvoke 将导致死锁。

    关于 EndInvoke 的一个重要注意事项:如果使用与匹配的 BeginInvoke 调用返回的 IAsyncResult 不同的 IAsyncResult 调用 EndInvoke,则在 .NET 版本中将导致 InvalidOperationException。同样的规则适用于此实现,传递给 EndInvokeIAsyncResult 必须是匹配的 BeginInvoke 返回的那个。这很重要,因为委托使用 CAsyncResult 作为 IAsyncResult,而同步调用使用 CThreadMethodEntry 作为 IAsyncResult

实现细节

委托(Delegates)

CDelegate

实现 IDelegate 相当直接,但是存在一些问题(你知道的,总会有一些问题,噢,我的意思是挑战)。首先,C++ 对可变参数的支持相当有限;我只知道 va_list 和使用重载模板来实现所需行为。其次,函数指针级别很低,并且它们不提供灵活性,使其在此上下文中难以适应。(C++0x 可能会改变这一切。)在研究了许多选项(例如,可以使用 Boost C++ 库)之后,我决定不为此伤脑筋,并选择了一个懒惰的解决方案来解决这两个问题:只使用一个参数并使委托签名预定义。副作用仅限于

  1. 来回转换参数和结果,以及
  2. 客户端被迫对所有委托(包括回调,它们也是委托)使用预定义的签名。
这些限制是由我的实现强制执行的,而不是由模式设计强制执行的。

创建新委托与使用 .NET Framework 相同,只需将函数指针传递给构造函数。但是,要创建类实例成员函数委托,您将需要 CClassDelegate。(详见下文。)

由于此实现将异步委托调用排队到线程池中,因此委托必须做的第一件事是初始化线程池。这基本上是在任何委托的第一个异步调用上完成的,但是可以将初始化函数设为公共并在启动应用程序时调用它。

要将新的异步调用排队,实现只需将新请求发布到线程池中

IAsyncResult *BeginInvoke(ULONG_PTR ulParam = 0, IDelegate *pCallback = NULL, 
                               LPVOID pvState = NULL, BOOL bFireAndForget = FALSE)
{
    // Create a new asynchronous request for this delegate call
    CAsyncResult *pAsyncResult = new CAsyncResult(this, ulParam, pCallback, 
                                                   pvState, bFireAndForget);
    BOOL bRes = pAsyncResult->Initialize();
    ATLASSERT(bRes != FALSE);

    // Queue the new request to be executed in the thread pool
    bRes = m_cThreadPool.QueueRequest(pAsyncResult);
    ATLASSERT(bRes != FALSE);

    return pAsyncResult;
}

CDelegateWorker

处理排队请求的机制位于符合 ATL Worker Archetype 的 CDelegateWorker 类中。这是一个非常简单、不言自明的类,其中 InitializeTerminate 对池中的每个线程调用一次,而 Execute 在每次线程循环时调用。(您可以阅读头文件 atlutil.h 获取实现细节;它是 ATL 的一部分。)

class CDelegateWorker
{
public:
    typedef CAsyncResult * RequestType;

    BOOL Initialize(LPVOID /*pvState*/) throw()
    {
        // Thread initialization code goes here
        // An example could be calling CoInitialize() to add COM support
        // to the thread
        return TRUE;
    }

    VOID Execute(
            CAsyncResult *pAsyncResult,
            LPVOID /*pvState*/,
            LPOVERLAPPED /*pOverlapped*/)
    {
        // A request is always initialized on the calling thread to
        // initialize the wait handle before moving to the receiving
        // thread (here)
        pAsyncResult->Execute();
        pAsyncResult->Terminate();
    }

    VOID Terminate(LPVOID /*pvState*/) throw()
    {
        // Thread cleanup code goes here
        // An example could be calling CoUninitialize()
    }
};

CClassDelegate<T>

现在您可能已经注意到我还没有提及将类成员函数指针传递给新的 CDelegate 的细节,这是另一个挑战 ;-)。这就是 CClassDelegate<T> 发挥作用的地方。为了接受类成员函数指针,CDelegate 需要使用模板,我不喜欢这一点,考虑到它是此设计实现中的核心类,并且会由于模板依赖性而影响许多其他类。因此,我决定创建一个小类来包装保留类成员函数指针的细节,并且由于该类使用模板,因此在 CDelegate 中使用它将需要向 CDelegate 添加模板,这将违背此类的目的。因此,创建了一个接口定义 IClassDelegate 以解耦模板依赖性。该接口基本上提供了一种调用类成员函数并自动删除 CClassDelegate 包装器的方法。所有这些意味着您可以这样做

VOID SomeClass::SomeFunction()
{
    CDelegate *pDelegate = new CDelegate(new CClassDelegate<SomeClass>(this,
                                             &SomeClass::SomeOtherFunction));
    ...
}

无需担心新的 CClassDelegate<T> 内存分配。事实上,在“即发即弃”场景中,您甚至不应该担心 pDelegate 的内存分配,因为它在适时也会自动删除。这一切都很好,但是为类实例成员函数创建委托使得每个声明都非常冗长,所以我还创建了一些宏来帮助使事情更简洁

VOID SomeClass::SomeFunction()
{
    MAKECLSDELEGATE(CMainDlg, SomeOtherFunction)->BeginInvoke(0, 
                              MAKECLSDELEGATE(CMainDlg, SomeCallback));
    ...
}

有三个宏可以简化委托的创建,它们是不言自明的

// Global delegate
#define MAKEDELEGATE(MEMBER) \
    (new CDelegate(&MEMBER))
// Class delegate
#define MAKECLSDELEGATE(CLASS, MEMBER) \
    (new CDelegate(new CClassDelegate(this, &CLASS::MEMBER)))
// Class instance delegate
#define MAKECLSINSTDELEGATE(CLASS, INSTANCE, MEMBER) \
    (new CDelegate(new CClassDelegate(INSTANCE, &CLASS::MEMBER)))

CSynchronizeInvoke

为了实现 ISynchronizeInvoke,我认为这是一个使用 Windows I/O 完成端口的绝佳机会。尽管 I/O 完成端口比在此实现中显示的更有用,但它们仍然提供了一种高效且简单的方法来将请求排队以在不同的线程中处理。这使得实现 ISynchronizeInvoke 变得轻而易举。以下是异步调用排队/处理的核心。(注意与排队 IDelegate 异步调用的相似之处。)

IAsyncResult *BeginInvoke(IDelegate *pDelegate, ULONG_PTR ulParam = 0, 
                          BOOL bFireAndForget = TRUE)
{
    // Create a new thread method entry for this call
    CThreadMethodEntry *pMethod = new CThreadMethodEntry(this, pDelegate, ulParam, 
                                                         bFireAndForget);
    BOOL bRes = pMethod->Initialize();
    ATLASSERT(bRes != FALSE);

    // Queue the new request for execution
    bRes = m_cPort.PostQueuedCompletionStatus(0, (ULONG_PTR)pMethod, NULL);
    ATLASSERT(bRes != FALSE);

    // Inform thread there is an asynchronous call pending
    ::PostThreadMessage(m_dwThreadId, WM_THREADCALLBACK, 0, 0);

    return pMethod;
}

处理挂起请求是通过调用 ProcessPendingInvoke() 完成的,该函数由消息循环机制(例如 WTL 中的 PreTranslateMessage)中的 IsThreadCallbackMessage 调用。默认情况下,该函数处理两个异步调用,以最大程度地减少在接收线程(可能是 GUI 线程)上执行函数调用所造成的干扰。如果您不在 GUI 线程上接收调用,则可以传递更多要处理的请求。

BOOL IsThreadCallbackMessage(MSG *pMsg)
{
    if (pMsg->message == WM_THREADCALLBACK)
    {
        ProcessPendingInvoke();
        return TRUE;
    }

    return FALSE;
}

VOID ProcessPendingInvoke(DWORD dwCount = 2)
{
    if (m_cPort.GetQueuedCount() < 1 )
        return;

    DWORD dwBytes = 0;                    // Unused
    ULONG_PTR ulKey = 0;
    LPOVERLAPPED pOverlapped = NULL;      // Unused

    // Retrieve one pending call at a time up to dwCount calls
    // If no calls are queued then this loop exits immediately
    while (dwCount-- > 0 && m_cPort.GetQueuedCompletionStatus(&dwBytes, &ulKey,
                                                              &pOverlapped, 0))
    {
        CThreadMethodEntry *pMethod = reinterpret_cast(ulKey);
        ATLASSERT(pMethod != NULL);

        // Execute & terminate the call
        pMethod->Execute();
        pMethod->Terminate();
    }
}

如您所见,这非常简单。使用 I/O 完成端口是个人选择,也可以使用不同的机制(即使用 WM_THREADCALLBACK 传递 pMethod)。我还考虑过使用带 QueueUserWorkItem 的 APC,但它只在调用线程和接收线程相同时才有效,这在这种情况下是无用的。

CAsyncResultCThreadMethodEntry

这两个类是异步调用的核心;它们包含与调用相关的所有信息,例如调用等待句柄、调用者指针等。请注意,它们的实现是根据 CDelegateWorker 的风格(InitializeExecuteTerminate)建模的。

有趣的是,.NET 实现中提到,客户端可以将委托的 BeginInvoke 返回的 IAsyncResult 强制转换为 AsyncResult 以访问与异步调用链接的其他资源。有关更多详细信息,请参阅 http://msdn.microsoft.com/en-us/library/system.runtime.remoting.messaging.asyncresult.aspx。以同样的方式,您可以将 IAsyncResult 强制转换为 CAsyncResult 用于委托调用,这非常有用,特别是用于使用 GetAsyncDelegate() 检索委托指针,以便您可以调用 EndInvoke() 来检索调用的返回值。

创建这两个类的唯一真正挑战是实现它们的 Terminate() 成员函数。一旦异步调用执行完毕,它就必须终止;也就是说,它必须检查调用是否已完成,解除分配资源,并且,可选地,在“即发即弃”场景中调用 EndInvoke()。此外,由于 CAsyncResult 用于委托异步调用,因此还需要支持回调函数。

以下分别是 CAsyncResultCThreadMethodEntryTerminate() 成员函数的实现

// CAsyncResult
VOID CAsyncResult::Terminate()
{
    // Flag the call as completed
    ATLASSERT(m_bIsCompleted == FALSE);
    m_bIsCompleted = TRUE;

    // Signal the call has completed
    // Signaling will delete us only if there is no callback
    // This is important because we are used as the callback
    // parameter
    ATLASSERT(m_hAsyncWaitEvent != NULL);
    BOOL bRes = ::SetEvent(m_hAsyncWaitEvent);
    ATLASSERT(bRes != FALSE);

    // Invoke the callback if one is provided
    if (m_pCallback != NULL)
    {
        m_pCallback->Invoke((ULONG_PTR)this);

        if (m_bFireAndForget)
            EndInvoke();

        // Since there was a callback EndInvoke() didn't delete
        // this request so delete it now
        delete this;
        // WARNING: Object has been deleted!

        return;
    }

    // WARNING: Object has been deleted passed EndInvoke!
    if (m_bFireAndForget)
        EndInvoke();
}

VOID CAsyncResult::EndInvoke()
{
    IAsyncResult *pAsyncResult = this;

    // Call EndInvoke, ignoring the return value
    ATLASSERT(m_pDelegate != NULL);
    m_pDelegate->EndInvoke(&pAsyncResult);
    ATLASSERT(pAsyncResult == NULL);
    // WARNING: This object is potentially deleted now!
    // Potentially meaning if m_pCallback != NULL
}

// CThreadMethodEntry
VOID CThreadMethodEntry::Terminate()
{
    // Flag the call as completed
    ATLASSERT(m_bIsCompleted == FALSE);
    m_bIsCompleted = TRUE;

    // Signal the call has completed
    // Signaling will delete us only if there is no callback
    ATLASSERT(m_hAsyncWaitEvent != NULL);
    BOOL bRes = ::SetEvent(m_hAsyncWaitEvent);
    ATLASSERT(bRes != FALSE);

    // If this is a fire and forget type of call then call
    // EndInvoke on behalf of the caller
    if (m_bFireAndForget)
    {
        IAsyncResult *pAsyncResult = this;

        // Call EndInvoke, ignoring the return value
        ATLASSERT(m_pCaller != NULL);
        m_pCaller->EndInvoke(&pAsyncResult);
        ATLASSERT(pAsyncResult == NULL);

        // WARNING: This object has now been deleted!
        // Don't access members of this object passed this point
    }
}

Using the Code

如果您使用 .NET Framework 使用过此模式,您会发现用 C++ 使用此实现非常容易。由于实现负责委托分配,因此您无需保留委托引用指针,这使其更接近 .NET 实现。此外,由于增加了对“即发即弃”场景的支持,因此可以轻松地只调用一个函数以避免阻塞当前线程。但是,以某种方式跟踪所有异步调用非常重要,尤其是在应用程序关闭期间,否则可能会发生意外行为。

异步委托调用

要异步调用委托,请创建 CDelegate 函数包装器,调用 BeginInvoke 启动调用,并调用 EndInvoke 终止调用。通常,如果您提供了回调函数,您会在回调函数中调用 EndInvoke。您通常会使用提供的宏创建所有委托。因此,假设您在类 CMainDlg 中定义了以下成员函数

ULONG_PTR CMainDlg::Test1(ULONG_PTR ulParam)
{
    ...
}

ULONG_PTR CMainDlg::Test1Callback(ULONG_PTR ulParam)
{
    ...
}

您将这样进行异步调用

MAKECLSDELEGATE(CMainDlg, Test1)->BeginInvoke(0, MAKECLSDELEGATE(CMainDlg, Test1Callback));

在回调函数中,您将检索函数的返回值

    // Retrieving Test1()'s return value
    CAsyncResult *pAsyncResult = (CAsyncResult *)ulParam;
    IDelegate *pDelegate = pAsyncResult->GetAsyncDelegate();
    UINT nRes = (UINT)pDelegate->EndInvoke((IAsyncResult **)&pAsyncResult);
    ATLASSERT(pAsyncResult == NULL);
    // NEVER use the async result after calling EndInvoke!
    ...

对于“即发即弃”场景,只需将 bFireAndForget 设置为 TRUE,委托将在异步调用完成后承担解除分配所有资源的全部责任

MAKECLSDELEGATE(CMainDlg, Test1)->BeginInvoke(0, MAKECLSDELEGATE(CMainDlg, Test1Callback), 
                                                                 NULL, TRUE);
    ...
// Never call EndInvoke()

同步调用

要将同步调用(类似于 WinForms 实现)与 WTL 一起使用,第一步是将您的窗口类派生自 CSynchronizeInvoke

class CMainDlg :
    public CDialogImpl,
    public CWinDataExchange,
    public CMessageFilter,
    public CSynchronizeInvoke
{
    ...

然后,一旦您的类已添加为 CMessageFilter,您需要检查消息泵中是否存在线程回调。这可以通过覆盖 PreTranslateMessage 来完成

virtual BOOL PreTranslateMessage(MSG *pMsg)
{
    // Process thread callback messages
    if (CSynchronizeInvoke::IsThreadCallbackMessage(pMsg))
        return TRUE;
    ...

现在,执行同步调用的所有机制都已就位。要进行同步调用,只需调用 BeginInvoke

    // Inside CMainDlg
    ...
    BeginInvoke(MAKECLSDELEGATE(CMainDlg, Test2), 0);
    // No need to call EndInvoke() as synchronized calls are implemented
    // as "fire & forget"

函数 Test2() 将在消息泵循环的下一次迭代中在同一线程上执行。

结论

如您所见,此实现与 Microsoft 异步设计模式的 .NET Framework 实现的这一部分之间存在差异。目标不是创建相同的实现,而是创建相似的实现,以提供一个非常相似的模式实现用于 C++。

实现的许多方面尚未讨论,例如使用等待句柄等待异步调用完成,轮询异步调用以确定它是否已完成等。尽管它们使用起来非常简单,但这些功能值得一提。请参阅演示应用程序,了解有关如何使用此实现及其功能的更多示例。在大多数情况下,如果您在 .NET 中使用过异步调用,您会发现此实现的特性非常易于使用,因为它们大部分都模仿了 .NET 实现。

未来想法和改进

此实现确实可以从错误处理和异常支持中受益。处理线程中引发的异常非常重要,应该像使用 .NET 时一样,在 try/catch 语句中调用 EndInvoke 来处理。

此外,此实现仅提供异步工作的基础知识。当然,会提出一些问题,例如“如何取消异步操作?”或“如何从异步操作报告进度?”等等。调用线程和接收线程之间的良好通信非常重要,ISynchronizeInvoke 有助于取得良好结果,但其使用级别仍然相当低。展望未来,可以实现一个 BackgroundWorker 类,它可以为取消异步操作和进度报告的一些问题提供解决方案。

关注点

网络上有很多关于异步设计模式的信息,以下是我书签列表中一些感兴趣的链接

历史

  • 2009年8月23日:首次发布
© . All rights reserved.