我的表单上有一个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是一种可怕的计时代码方式。我只是想知道发生了什么。
答案 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()。