为什么在计时器回调中调用事件会导致以下代码被忽略?

And*_*mos 2 .net c# events timer winforms

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

问题是我在调用DeliveryProgressChangedEvent事件后放置的任何代码似乎都被忽略了。我的计数器永远不会增加,因此允许计时器永远运行。

如果我在增加计数器后调用该事件,则一切正常。调用事件后什么都不会执行。如果不解决这个问题,我想了解而不是走简单的路线。

我对 system.threading 计时器对象和事件进行了大量研究,但找不到与我的问题相关的任何信息。

我为我的项目创建了一个简单的例子来演示下面的问题。

游戏类

    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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

表单类

    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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

编辑

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

Jim*_*imi 5

问题
使用System.Threading.Timer类,当TimerCallback被调用时,事件被触发,以通知的用户DeliveryProgressChangedEventDeliveryCompletedEvent定制Game类的程序的进展情况和它的终止。

在示例类中,订阅者(此处为 Form 类)更新 UI、设置 ProgressBar 控件的值并显示 MessageBox(此处所示类示例的实际实现中使用)。

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

DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
++_counter;
Run Code Online (Sandbox Code Playgroud)

_counter永远不会到达应该增加的行,因此检查_counter将计时器设置为新值的代码永远不会执行。

会发生什么

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

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

    当 MessageBox 显示时——它是一个模态窗口——它会像往常一样从它运行的地方阻止线程。永远不会到达调用事件的行后面的代码,因此_counter永远不会增加:

     DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
     ++_counter;
    
    Run Code Online (Sandbox Code Playgroud)
  2. System.Threading.Timer可以由多个线程提供服务。我只是在这一点上引用文档,它非常简单:

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

    在实践中发生的情况是,虽然执行 CallBack 的线程被 MessageBox 阻塞,但这并不能阻止 Timer 从另一个线程执行 CallBack:调用事件时会显示一个新的 MessageBox,并且一直运行直到它有资源。

  3. MessageBox 没有所有者。当 MessageBox 显示时没有指定所有者,它的类使用GetActiveWindow()来查找 MessageBox 窗口的所有者。此函数尝试返回附加到调用线程的消息队列的活动窗口的句柄。但是运行 MessageBox 的线程没有活动窗口,因此所有者是桌面(实际上,IntPtr.Zero在这里)。

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

如何解决

  1. 当然,使用另一个Timer。该System.Windows.Forms.Timer(的WinForms)或DispatcherTimer(WPF)是天然的替代品。它们的事件在 UI 线程中引发。

? 此处提供的代码只是用于重现问题的 WinForms 实现,因此这些代码可能不适用于所有上下文。

  1. 使用System.Timers.TimerSynchronizingObject属性提供了将事件编组回创建当前类实例的线程的方法(与具体实现上下文相关的考虑相同)。

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

  3. BeginInvoke() 消息框,附加到 UI 线程SynchronizationContext。例如,:

     this.BeginInvoke(new Action(() => MessageBox.Show(this, "Delivery Completed")));
    
    Run Code Online (Sandbox Code Playgroud)

    现在 MessageBox 归 Form 所有,它将像往常一样运行。ThreadPool 线程可以自由继续:Modal Window 与 UI 线程同步。

  4. 避免将 MessageBox 用于此类通知,因为它真的很烦人 :) 有许多其他方法可以通知用户状态更改。MessageBox 可能不太周到

为了让它们按预期工作,而不改变当前的实现,可以像这样重构GameForm1类:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)