为什么TimerKick事件中的SendKeys.SendWait和Thread.Sleep没有阻塞?

时间:2013-01-12 08:36:05

标签: c# .net winforms timer blocking

我的表单上有一个Timer控件,带有以下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 runs on the UI thread开始,我希望事件处理程序能够阻止在运行时发生的Tick个事件,这些事件会将AndrewAndrewAndrew...作为输出。相反,我得到AnAnAnAn...

为什么后续的Tick事件会在第一个完成之前提出并处理?

如何确保Timer一次引发一个事件,并在处理程序运行完成之前完全阻塞?

我意识到Thread.Sleep是一种可怕的计时代码方式。我只是想知道发生了什么。

2 个答案:

答案 0 :(得分:1)

您是通过消息循环重新进入的受害者。您通过消息循环间接递归到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中只有一个线程泵送消息,按顺序处理排队的每个项目。但是,当放入队列的方法本身可以泵送消息时,情况并非如此。在该示例中,第3个操作实际上在第2个操作之前完成。 DoComplicatedWork方法类似于SendWait中发生的情况。

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

答案 1 :(得分:1)

您可以使用Reference Source或一个好的反编译器(如Reflector或ILSpy)来查找框架代码中的内容。 SendKeys.SendWait()方法最终调用内部类上的方法调用SKWindow来实现“等待”请求。该方法如下所示:

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

DoEvents()是一种臭名昭着的方法,以崩溃 lot 代码而闻名。你在计时器的Tick事件处理程序上的重新进入是相当无害的,这段代码可以对你的程序造成更多很多的伤害。你会发现this answer中解释的更常见的肮脏。显然,如果可以,你会想要避免使用SendWait()。