为什么是SendKeys.SendWait和Thread.在没有阻塞的Timer_Tick事件中睡觉

本文关键字:Timer Tick 事件 SendWait SendKeys Thread 为什么 | 更新日期: 2023-09-27 17:51:23

我有一个定时器控制我的表单与以下Tick事件处理程序:

private void timer1_Tick(object sender, EventArgs e) {
    foreach (char c in "Andrew") {
        SendKeys.SendWait(c.ToString());
        System.Threading.Thread.Sleep(1000);
    }
}

由于System.Windows.Forms.Timer在UI线程上运行,我希望事件处理程序阻止进一步的Tick事件发生,而它正在运行,这将给AndrewAndrewAndrew...作为输出。相反,我得到AnAnAnAn...

为什么后续的Tick事件在第一个事件完成之前被引发和处理?

我如何确保定时器一次引发一个事件,并得到完全阻塞,直到处理程序已经运行到完成?

我意识到线程。睡眠是计时代码的糟糕方式。我只是想知道发生了什么事。

为什么是SendKeys.SendWait和Thread.在没有阻塞的Timer_Tick事件中睡觉

您是通过消息循环重新进入的受害者。您通过消息循环间接递归到timer1_Tick函数。正在发生的事情是,在SendKeys.SendWait 内部,另一个消息循环正在启动(不是在不同的线程上),以监视消息是否已被处理。然后在另一个线程上,当消息在这个内部循环中被处理时,计时器正在触发并发布消息以再次调用tick函数。欢闹。

也许一个简单的例子会有帮助。运行此命令并观察输出。

public class Program
{
    private static Queue<Action> queue = new Queue<Action>();
    public static void Main(string[] args)
    {
        // put three things in the queue. 
        // In a simple world, they would finish in order.
        queue.Enqueue(() => DoWork(1));
        queue.Enqueue(() => DoComplicatedWork(2));
        queue.Enqueue(() => DoWork(3));
        PumpMessages();            
    }
    private static void PumpMessages()
    {
        while (queue.Count > 0)
        {
            Action action = queue.Dequeue();
            action();
        }
    }
    private static void DoWork(int i)
    {
        Console.WriteLine("Doing work: {0}", i);
    }
    private static void DoComplicatedWork(int i)
    {
        Console.WriteLine("Before doing complicated work: {0}", i);
        PumpMessages();
        Console.WriteLine("After doing complicated work: {0}", i);
    }
}`

你在某种程度上假设,因为在UI中只有一个线程抽取消息,所以排队的每个项目都是按顺序处理的。但是,当放入队列的方法本身可以泵送消息时,情况就不一样了。在这个例子中,第三个操作实际上在第二个操作之前完成。DoComplicatedWork方法与SendWait类似。

我应该回答你关于如何防止这种情况的第二个问题。lock没有帮助,因为它们是可重入的(即同一个线程可以多次获得锁)。最好的方法是在方法内部禁用计时器或分离tick处理程序,并在返回之前重新启用/附加处理程序。您也可以尝试一个简单的boolean标志来指示您是否已经在方法中,如果是,则返回。

您可以使用参考源代码或好的反编译器(如Reflector或ILSpy)来查找框架代码内部发生的事情。SendKeys.SendWait()方法最终调用调用SKWindow的内部类上的方法来实现"等待"请求。这个方法看起来像这样:

public static void Flush()
{
    Application.DoEvents();
    while ((events != null) && (events.Count > 0))
    {
        Application.DoEvents();
    }
}

DoEvents()是一个相当臭名昭著的方法,以崩溃大量代码而闻名。计时器的Tick事件处理程序的重入性是相当无害的,这段代码可以对程序造成很多大的损害。你会发现在这个答案中解释了更常见的肮脏。显然,如果可以的话,您希望避免使用SendWait()。