为什么在计时器回调中调用事件会导致以下代码被忽略? [英] Why does invoking an event in a timer callback cause following code to be ignored?

查看:85
本文介绍了为什么在计时器回调中调用事件会导致以下代码被忽略?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在编写一个简单的游戏,该游戏使用system.threading命名空间中的计时器来模拟操作的等待时间.我的目标是使计时器每秒执行一次,持续x秒钟.为此,我在计时器回调中添加了一个计数器.

I'm writing a simple game that uses timers from the system.threading namespace to simulate wait times for actions. My goal is to have the timer execute once every second for x amount of seconds. To achieve this I added a counter in the timer callback.

问题是我调用DeliveryProgressChangedEvent事件后放置的任何代码似乎都被忽略了.我的计数器永远不会增加,从而使计时器永远运行.

The problem is any code I place after invoking the DeliveryProgressChangedEvent event seems to get ignored. My counter is never incremented thus allowing the timer to run forever.

如果我在增加计数器后调用事件,则一切正常.调用事件后什么也不会执行.如果不解决此问题,我不想走简单的道路.

If I invoke the event after I increment the counter, everything works fine. Just nothing after invoking the event will execute. Rather than going the easy route I'd like to understand if not resolve this problem.

我对system.threading计时器对象以及事件做了相当多的研究,但是找不到与我的问题有关的任何信息.

I did a fair bit of research into the system.threading timer object as well as events but wasn't able to find any information related to my issue.

我创建了一个简单的项目示例来演示以下问题.

I created a bare bones example of my project to demonstrate the issue below.

    class Game
    {
        private Timer _deliveryTimer;
        private int _counter = 0;

        public event EventHandler DeliveryProgressChangedEvent;
        public event EventHandler DeliveryCompletedEvent;

        public Game()
        {
            _deliveryTimer = new Timer(MakeDelivery);
        }

        public void StartDelivery()
        {
            _deliveryTimer.Change(0, 1000);
        }

        private void MakeDelivery(object state)
        {
            if (_counter == 5)
            {
                _deliveryTimer.Change(0, Timeout.Infinite);
                DeliveryCompletedEvent?.Invoke(this, EventArgs.Empty);
            }

            DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);

            ++_counter;
        }
    }

表单类

    public partial class Form1 : Form
    {
        Game _game = new Game();

        public Form1()
        {
            InitializeComponent();

            _game.DeliveryProgressChangedEvent += onDeliveryProgressChanged;
            _game.DeliveryCompletedEvent += onDeliveryCompleted;

            pbDelivery.Maximum = 5;
        }

        private void onDeliveryProgressChanged(object sender, EventArgs e)
        {
            if (InvokeRequired)
                pbDelivery.BeginInvoke((MethodInvoker)delegate { pbDelivery.Increment(1); });

            MessageBox.Show("Delivery Inprogress");
        }

        private void onDeliveryCompleted(object sender, EventArgs e)
        {
            MessageBox.Show("Delivery Completed");
        }

        private void button1_Click(object sender, EventArgs e)
        {
            _game.StartDelivery();
        }
    }

编辑

只是为了澄清我的意思.我在DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);之后加上的任何代码都不会执行.在我的示例中,++_counter将不会运行.该事件确实会触发,并且onDeliveryProgressChanged处理程序也会运行.

Just to clarify what I mean. Any code I put after DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty); will not execute. In my example ++_counter will not run. The event does fire and the onDeliveryProgressChanged handler does run.

推荐答案

问题:
使用 System.Threading.Timer 类调用 TimerCallback ,引发事件,以将自定义Game类的 DeliveryProgressChangedEvent DeliveryCompletedEvent 的订户通知过程进度以及该过程的终止.

The problem:
Using a System.Threading.Timer class, when the TimerCallback is called, events are raised, to notify the subscribers of the DeliveryProgressChangedEvent and DeliveryCompletedEvent of custom Game class of the progress of a procedure and the termination of it.

在示例类中,订阅者(这里是Form类)将更新UI,设置ProgressBar控件的值并显示MessageBox(用于此处显示的类示例的实际实现中).

In the sample class, the subscriber (a Form class, here) updates an UI, settings the value of a ProgressBar control and also showing a MessageBox (used in the actual implementation of the class sample shown here).

似乎在调用第一个事件之后:

It appears that after the first event is invoked:

DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
++_counter;

永远不会到达应该增加_counter的行,因此永远不会执行检查_counter以将Timer设置为新值的代码.

the line where the _counter should be increased is never reached, thus the code that inspects the _counter to set the Timer to a new value is never executed.

会发生什么:

  1. System.Threading.Timer由ThreadPool线程(一个以上)提供服务.在UI线程以外的其他线程上调用其回调.从回调调用的事件也会在ThreadPool线程中引发.
    然后,在同一线程上运行处理程序委托onDeliveryProgressChanged中的代码.

  1. The System.Threading.Timer is served by ThreadPool threads (more than one). Its callback is called on a thread other than the UI thread. The events invoked from the callback are also raised in a ThreadPool thread.
    The code in the handler delegate, onDeliveryProgressChanged, is then run on the same Thread.

 private void onDeliveryProgressChanged(object sender, EventArgs e)
 { 
     if (InvokeRequired)
         pbDelivery.BeginInvoke((MethodInvoker)delegate { pbDelivery.Increment(1); });
     MessageBox.Show("Delivery Inprogress");
 }

当显示MessageBox时-这是一个Modal窗口-像往常一样,它从运行线程的位置阻止Thread.永远不会到达调用事件的行之后的代码,因此永远不会增加_counter:

When the MessageBox is shown - it's a Modal Window - it blocks the Thread from where it's run, as usual. The code following the line where the event is invoked is never reached, so _counter is never increased:

 DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
 ++_counter;

  • System.Threading.Timer可以由多个线程服务.关于这一点,我只是引用文档,这很简单:

  • The System.Threading.Timer can be served by more than one thread. I'm just quoting the Docs on this point, it's quite straightforward:

    由计时器执行的回调方法应该是可重入的,因为 在ThreadPool线程上调用它.回调可以执行 如果计时器间隔为,则同时在两个线程池线程上 少于执行回调所需的时间,或者如果所有线程 池线程正在使用,并且回调多次排队.

    The callback method executed by the timer should be reentrant, because it is called on ThreadPool threads. The callback can be executed simultaneously on two thread pool threads if the timer interval is less than the time required to execute the callback, or if all thread pool threads are in use and the callback is queued multiple times.

    实际上,发生的情况是,执行CallBack的线程被MessageBox阻止了,但这并没有阻止Timer从另一个线程执行CallBack:当事件发生时,将显示一个新的MessageBox.调用后,它将一直运行,直到拥有资源为止.

    What happens, in practice, is that while the Thread where the CallBack is executed, is blocked by the MessageBox, this doesn't stop the Timer from executing the CallBack from another thread: a new MessageBox is shown when the event is invoked and it keeps on running until it has resources.

    MessageBox没有所有者.当显示消息框而不指定所有者时,其类使用

    The MessageBox has no Owner. When a MessageBox is shown without specifying the Owner, its class uses GetActiveWindow() to find an Owner for the MessageBox Window. This function tries to return the handle of the active window attached to the calling thread's message queue. But the thread from which the MessageBox is run has no active Window, as a consequence, the Owner is the Desktop (IntPtr.Zero).

    可以通过激活(单击)调用MessageBox的表单来手动进行验证:MessageBox窗口将消失在Form下,因为它不属于 它.

    This can be manually verified by activating (clicking on) the Form where the MessageBox is called: the MessageBox Window will disappear under the Form, since it's not owned by it.

    如何解决:

    1. 当然,可以使用另一个计时器. System.Windows.Forms.Timer (WinForms)或 DispatcherTimer (WPF )是 natural 的替代物.他们的事件在UI线程中引发.
    1. Of course, using another Timer. The System.Windows.Forms.Timer (WinForms) or the DispatcherTimer (WPF) are the natural substitutes. Their events are raised in the UI Thread.

    ►此处提供的代码只是WinForms实现,用于 重现问题,因此这些可能并不适用于所有情况.

    ► The code presented here is just a WinForms implementation made to reproduce a problem, hence these may not apply to all contexts.

    1. 使用 System.Timers.Timer : SynchronizingObject 属性提供意味着将事件封送回创建当前类实例的线程(与具体实现上下文有关的考虑).

    1. Use a System.Timers.Timer: the SynchronizingObject property provides means to marshal the events back to the Thread that created the current class instance (same consideration in relation to the concrete implementation context).

    使用生成 AsyncOperation AsyncOperationManager.CreateOperation()方法,然后使用 SendOrPostCallback 委托让调用 SynchronizationContext.Post()方法(经典的BackGroundWorker样式).

    Generate an AsyncOperation using the AsyncOperationManager.CreateOperation() method, then use a SendOrPostCallback delegate to let the AsyncOperation call the SynchronizationContext.Post() method (classic BackGroundWorker style).

    BeginInvoke() MessageBox,将其附加到UI线程SynchronizationContext.例如:

    BeginInvoke() the MessageBox, attaching it to the UI Thread SynchronizationContext. E.g.,:

     this.BeginInvoke(new Action(() => MessageBox.Show(this, "Delivery Completed")));
    

    现在,MessageBox由Form拥有,它将像往常一样运行. ThreadPool线程可以自由继续:模态窗口与UI线程同步.

    Now the MessageBox is owned by the Form and it will behave as usual. The ThreadPool thread is free to continue: the Modal Window is synched with the UI Thread.

    避免使用MessageBox进行此类通知,因为它确实很烦人:)还有许多其他方式可以将状态更改通知用户. MessageBox可能不是考虑周全的.

    Avoid using a MessageBox for this kind of notifications, since it's really annoying :) There are many other ways to notify a User of status changes. The MessageBox is probably the less thoughtful.

    要使它们按预期工作,而无需更改当前实现,可以按如下方式重构GameForm1类:

    To make them work as intended, without changing the current implementation, the Game and Form1 classes can be refactored like this:

    class Game
    {
        private System.Threading.Timer deliveryTimer = null;
        private int counter;
    
        public event EventHandler DeliveryProgressChangedEvent;
        public event EventHandler DeliveryCompletedEvent;
    
        public Game(int eventsCount) { counter = eventsCount; }
    
        public void StartDelivery() {
            deliveryTimer = new System.Threading.Timer(MakeDelivery);
            deliveryTimer.Change(1000, 1000);
        }
    
        public void StopDelivery() {
            deliveryTimer?.Dispose();
            deliveryTimer = null;
        }
    
        private void MakeDelivery(object state) {
            if (deliveryTimer is null) return;
            DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
            counter -= 1;
    
            if (counter == 0) {
                deliveryTimer?.Dispose();
                deliveryTimer = null;
                DeliveryCompletedEvent?.Invoke(this, EventArgs.Empty);
            }
        }
    }
    
    
    public partial class Form1 : Form
    {
        Game game = null;
    
        public Form1() {
            InitializeComponent();
            pbDelivery.Maximum = 5;
    
            game = new Game(pbDelivery.Maximum);
            game.DeliveryProgressChangedEvent += onDeliveryProgressChanged;
            game.DeliveryCompletedEvent += onDeliveryCompleted;
        }
    
        private void onDeliveryProgressChanged(object sender, EventArgs e)
        {
            this.BeginInvoke(new MethodInvoker(() => {
                pbDelivery.Increment(1);
                // This MessageBox is used to test the progression of the events and
                // to verify that the Dialog is now modal to the owner Form.  
                // Of course it's not used in an actual implentation.  
                MessageBox.Show(this, "Delivery In progress");
            }));
        }
    
        private void onDeliveryCompleted(object sender, EventArgs e)
        {
            this.BeginInvoke(new Action(() => MessageBox.Show(this, "Delivery Completed")));
        }
    
        private void button1_Click(object sender, EventArgs e)
        {
            game.StartDelivery();
        }
    }
    

    这篇关于为什么在计时器回调中调用事件会导致以下代码被忽略?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

  • 查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆