在非托管主机下的托管组件中获取一部分空闲处理

nos*_*tio 5 .net c# com winforms async-await

我有一个用 C# 编写的托管组件,它由旧版 Win32 应用程序作为 ActiveX 控件托管。在我的组件内部,我需要能够获取通常的Application.Idle事件,即获取 UI 线程上空闲处理时间的时间片(它必须是主 UI 线程)。

然而,在这个托管场景中,Application.Idle不会被解雇,因为没有托管消息循环(即没有Application.Run)。

可悲的是,主机也没有实现IMsoComponentManager,这可能适合我的需要。由于许多充分的理由,冗长的嵌套消息循环(带有Application.DoEvents)不是一个选择。

到目前为止,我能想到的唯一解决方案是使用普通的Win32 计时器。根据这篇(现已消失的)MSKB 文章WM_TIMER具有最低优先级之一,其次是WM_PAINT,这应该让我尽可能接近空闲状态。

对于这种情况,我是否缺少其他选项?

这是原型代码:

// Do the idle work in the async loop

while (true)
{
    token.ThrowIfCancellationRequested();

    // yield via a low-priority WM_TIMER message
    await TimerYield(DELAY, token); // e.g., DELAY = 50ms

    // check if there is a pending user input in Windows message queue
    if (Win32.GetQueueStatus(Win32.QS_KEY | Win32.QS_MOUSE) >> 16 != 0)
        continue;

    // do the next piece of the idle work on the UI thread
    // ...
}       

// ...
    
static async Task TimerYield(int delay, CancellationToken token) 
{
    // All input messages are processed before WM_TIMER and WM_PAINT messages.
    // System.Windows.Forms.Timer uses WM_TIMER 
    // This could be further improved to re-use the timer object

    var tcs = new TaskCompletionSource<bool>();
    using (var timer = new System.Windows.Forms.Timer())
    using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: true))
    {
        timer.Interval = delay;
        timer.Tick += (s, e) => tcs.TrySetResult(true);
        timer.Enabled = true;
        await tcs.Task;
        timer.Enabled = false;
    }
}

    
Run Code Online (Sandbox Code Playgroud)

我认为不Task.Delay适合这种方法,因为它使用内核计时器对象,这些对象独立于消息循环及其优先级。

更新后,我发现又一个选项: WH_FOREGROUNDIDLE/ForegroundIdleProc。看起来和我需要的一模一样。

更新后,我还发现WPF使用Win32 计时器技巧进行低优先级调度程序操作,即Dispatcher.BeginInvoke(DispatcherPriority.Background, ...)

nos*_*tio 3

嗯,WH_FOREGROUNDIDLE/ForegroundIdleProc钩子很棒。它的行为方式非常类似于Application.Idle:当线程的消息队列为空时,钩子被调用,并且底层消息循环的GetMessage调用即将进入阻塞等待状态。

然而,我忽略了一件重要的事情。事实证明,我正在处理的主机应用程序有自己的计时器,并且它的 UI 线程WM_TIMER不断且非常频繁地发送消息。如果我一开始就用 Spy++ 查看它,我就可以了解到这一点。

For ForegroundIdleProc(以及 for Application.Idle,就此而言),WM_TIMER与任何其他消息没有什么不同。WM_TIMER在调度每个新的队列并且队列再次变空后,将调用该钩子。这导致ForegroundIdleProc我接到的电话比我真正需要的要多得多。

不管怎样,尽管有外来计时器消息,ForegroundIdleProc回调仍然表明线程队列中没有更多的用户输入消息(即键盘和鼠标空闲)。因此,我可以开始我的闲置工作,并使用async/实现一些限制逻辑await,以保持 UI 响应。这就是它与我最初的基于计时器的方法的不同之处。