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, ...):
嗯,WH_FOREGROUNDIDLE/ForegroundIdleProc钩子很棒。它的行为方式非常类似于Application.Idle:当线程的消息队列为空时,钩子被调用,并且底层消息循环的GetMessage调用即将进入阻塞等待状态。
然而,我忽略了一件重要的事情。事实证明,我正在处理的主机应用程序有自己的计时器,并且它的 UI 线程WM_TIMER不断且非常频繁地发送消息。如果我一开始就用 Spy++ 查看它,我就可以了解到这一点。
For ForegroundIdleProc(以及 for Application.Idle,就此而言),WM_TIMER与任何其他消息没有什么不同。WM_TIMER在调度每个新的队列并且队列再次变空后,将调用该钩子。这导致ForegroundIdleProc我接到的电话比我真正需要的要多得多。
不管怎样,尽管有外来计时器消息,ForegroundIdleProc回调仍然表明线程队列中没有更多的用户输入消息(即键盘和鼠标空闲)。因此,我可以开始我的闲置工作,并使用async/实现一些限制逻辑await,以保持 UI 响应。这就是它与我最初的基于计时器的方法的不同之处。