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

防止第三方库中意外的消息泵送 - DirectShow 示例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (3投票s)

2009 年 6 月 1 日

CPOL

3分钟阅读

viewsIcon

28208

downloadIcon

152

本文展示了一种简洁的解决方案,用于解决第三方代码意外泵送消息的问题。

引言

这个项目演示了窗口应用程序设计中固有的一个问题和一个简单的解决方案。问题是,当你调用一个第三方库时,无法保证该库不会调用 Application.DoEvents() 或以其他方式导致你的应用程序的消息队列泵送。这通常不是问题,但它可能会在你的代码中设置一个竞争条件。

例如,假设用户连续快速点击一个按钮多次。你的代码正忙于处理第一次按钮点击,并且你的应用程序处于不一致的状态。你可能(有道理地)认为,在你的按钮事件处理程序中,你完全可以安全地避免被另一次按钮点击抢占。毕竟,你正在使用单线程模型,事件队列会序列化用户输入。在按钮点击事件处理程序中,你随后调用一些第三方代码。该第三方代码由于某种原因调用了 Application.DoEvents() 或在本地代码中泵送消息。突然,下一个按钮点击事件被触发,并且由于第一次点击的部分运行事件处理程序留下的不一致状态,非常快地抛出一个异常。

背景

我在开发一个使用 DirectShow 进行视频录制的 C# WinForms 应用程序时遇到了这个问题。这个问题是由一位测试人员发现的,他快速点击各种控制按钮并导致了一个空引用异常。问题的诊断记录在 UseNet 线程 这里

我考虑过其他解决方案,例如在处理用户输入或计时器消息期间删除所有用户输入和计时器消息。这个解决方案可以防止重入,但删除消息可能会导致其他问题。

解决方案

为了防止事件处理程序的嵌套,我们需要阻止第三方代码(在本例中为 DirectShow 代码)中的消息循环从你的应用程序中的窗口取消排队和处理消息。我用来做到这一点的解决方案是启动一个带有自己消息循环的第二个线程。此代码片段来自应用程序的 Main() 方法

SynchronizationContext context = null;
bool started = false;

ThreadStart startRoutine =
    delegate
    {
        // Set the sync context on the thread
        context = new WindowsFormsSynchronizationContext(); 
        SynchronizationContext.SetSynchronizationContext(context);

        // Run a dedicated message loop with no forms
        Application.Run();
    };

// Start up the DS runner
Thread runner = new Thread(startRoutine);
runner.Start();

请注意,在新线程上创建并设置了一个同步上下文。这是为了允许使用 SynchronizationContext.Send() 进行简单的线程间消息传递。只需将对第三方库(泵送消息的库)的调用放在 SendOrPostCallback 委托中,并将其 Send() 到另一个线程。

SendOrPostCallback a =
    delegate
    {
        //Call code that pumps messages here
        ......
    };

context.Send(a, null);

一个细节是使用了 WindowsFormsSynchronizationContext,而不是普通的 SynchronizationContext。问题是 SynchronizationContext.Send() 实际上是在调用线程上执行 SendOrPostCallbackWindowsFormsSynchronizationContext.Send() 在目标线程上正确地执行它。

运行演示

演示应用程序有两个按钮。第一个按钮首先在主线程的消息队列上排队一条消息,然后在主线程上运行 DirectShow 调用。当 DirectShow 调用进行时,排队的消息被处理,嵌套在按钮点击内部。

第二个按钮在一个专用线程上同步运行 DirectShow 调用。这意味着排队的消息直到按钮点击处理程序返回后才会被处理。

关注点

应该注意的是,这里演示的问题解决方案尚未准备好投入生产。如果使用它,你将需要以某种方式将其打包到辅助类中。你还需要考虑在第二个线程中抛出的任何异常会发生什么(提示:如果使用 Send(),它们将返回到主线程;如果使用 Post(),它们需要在第二个线程上处理)。

免责声明

我不认为自己是 DirectShow 或 Windows 编程细节(无论是 Win32 还是 .NET)方面的专家。提交本文是希望它可以帮助其他人避免相同的情况,同时也希望更多的专家程序员能够提出其他解决方案。

© . All rights reserved.