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

2024-02-12

我正在编写一个简单的游戏,使用来自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;
        }
    }

表格类

    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();
        }
    }

EDIT

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


问题:
用一个系统.线程.定时器 https://learn.microsoft.com/en-us/dotnet/api/system.threading.timer类,当定时器回调 https://learn.microsoft.com/en-us/dotnet/api/system.threading.timercallback被调用,事件被引发,以通知订阅者DeliveryProgressChangedEvent and DeliveryCompletedEvent定制的Game过程的进展和终止的类别。

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

看来在调用第一个事件后:

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

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

会发生什么:

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

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

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

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

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

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

  3. MessageBox 没有所有者。当显示 MessageBox 时未指定所有者,其类使用获取活动窗口() https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getactivewindow找到 MessageBox 窗口的所有者。该函数尝试返回附加到调用线程的消息队列的活动窗口的句柄。但是运行 MessageBox 的线程没有活动窗口,因此,所有者是桌面(实际上,IntPtr.Zero here).

这可以是manually通过激活(单击)调用 MessageBox 的表单来验证:MessageBox 窗口将在表单下方消失,因为它不是owned by it.

怎么解决:

  1. 当然,使用另一个定时器。这系统.Windows.Forms.定时器 https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.timer(WinForms) 或调度定时器 https://learn.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatchertimer(WPF)是natural替代品。它们的事件在 UI 线程中引发。

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

  1. Use a 系统.定时器.定时器 https://learn.microsoft.com/en-us/dotnet/api/system.timers.timer: the 同步对象 https://learn.microsoft.com/en-us/dotnet/api/system.timers.timer.synchronizingobject属性提供了将事件编组回创建当前类实例的线程的方法(与具体实现上下文相关的考虑相同)。

  2. 生成一个异步操作 https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.asyncoperation使用AsyncOperationManager.CreateOperation() https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.asyncoperationmanager.createoperation方法,然后使用发送或后回调 https://learn.microsoft.com/en-us/dotnet/api/system.threading.sendorpostcallback委托让AsyncOperation打电话给SynchronizationContext.Post() https://learn.microsoft.com/en-us/dotnet/api/system.threading.synchronizationcontext.post方法(经典的 BackGroundWorker 风格)。

  3. 开始调用() https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.begininvoke消息框,附着它到 UI 线程SynchronizationContext. E.g.,:

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

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

  4. 避免使用 MessageBox 进行此类通知,因为它确实很烦人:) 还有许多其他方法可以通知用户状态更改。 MessageBox 可能比较少周到.

为了使它们按预期工作,而不改变当前的实现,Game and Form1类可以这样重构:

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();
    }
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • WebClient.UploadString 方法的最大数据长度

    我的 asp net mvc 应用程序中有以下代码 string URI http send url com smsapi sender php string queryParameters a long query string stri
  • scipy curve_fit多系列数据

    我试图建立一个曲线拟合 考虑基于相同 x 值和相同 指数 定律的多个 y 系列 该系列中的 y 值略有不同 因为它们是实验性的 但仍然很接近 x 相同 我尝试构建两个数组 一个包含 x 另一个包含两个不同系列的 y def f x a b
  • ContentResolver.bulkInsert(..) 的意义是什么?

    My goal 我想批量 以事务方式 将多条记录插入到sqlite中 我的问题 我找到了方法android content ContentResolver 批量插入 http android git kernel org p platfor
  • PHP 中的位操作和 MySQL 检索

    我正在尝试稍微优化我的 mysql 表 以获得一个更易于管理的表 我想将用户权限存储在一个位字段中 例如 用户权限可以是 0110 我的用户权限数量越来越多 因此长度可能会长一点 该示例可能对应于以下内容 0 用户不能在网站上发布新闻 1
  • Groovy 中的爬虫(JSoup VS Crawler4j)

    我希望在 Groovy 中开发一个网络爬虫 使用 Grails 框架和 MongoDB 数据库 它能够爬取网站 创建网站 URL 及其资源类型 内容 响应时间和涉及的重定向数量的列表 我正在争论 JSoup 与 Crawler4j 我已经阅
  • HTML5 日期输入 6 位数年份

    我有一个标准
  • 设置 QMessageBox 的父级

    我不明白设置父级有什么好处QMessageBox 例如在以下代码中 void mainWindow showMessage QString msg QMesageBox information this title msg this is
  • 如何创建批处理文件来在Cmder中执行命令?

    我想创建一个启动 Cmder 的批处理文件 然后在 Cmder 中执行一些命令 我知道如何使用批处理文件启动 Cmder 但不知道如何使用批处理文件在 Cmder 中编写 执行命令 我尝试这个 echo off cd C Program F
  • 将 html 内联图像从浏览器复制/粘贴到文字处理器

    我正在尝试使用 html 内联图像 背景 尝试创建我自己的 CMS 它不会将图像保留为单独的文件 我可以将此类图像从浏览器 Firefox IE 复制 粘贴到 Photoshop 或 MS Paint 等图像处理程序 但不能复制到 MS W
  • 使用 svn+ssh 协议通过 2 跳访问 Subversion 存储库

    我的 Ubuntu Subversion 服务器无法直接访问互联网 192 168 1 2 我的公共 Ubuntu 机器通过 DMZ 暴露在 192 168 1 1 我已经设置了从 192 168 1 1 3906 到 192 168 1
  • C 中的“double”运算和优化

    我最近分析了一段用 VS2005 编译的旧代码 因为 调试 无优化 和 发布 O2 Oi Ot 选项 编译中的数值行为不同 简化的 代码如下所示 void f double x1 double y1 double x2 double y2
  • YII 2.0 GridView 更新

    我在通过 javascript 更新 yiigridview 时遇到问题 我正在尝试以 yii 1 1 方式使用它 jQuery fn yiigridview update grid id 但这给我带来了错误 undefined is no
  • 管道阶段规范对象必须恰好包含一个带有 php mongo 聚合的字段

    我正在尝试将聚合与项目 匹配和排序一起使用 但出现异常 MongoResultException准确地说 说 exception A pipeline stage specification object must contain exac
  • php oop 从同一类的方法内部调用方法

    我有以下问题 class class name function b do something function c function a call function b 当我像往常一样调用函数时 this gt b 我收到此错误 Usin
  • 如何避免页面刷新时的按钮事件

    我有 aspx 页面 该页面通过单击按钮将数据插入数据库 但当我按下按钮时 它就正常了 我收到 成功插入数据 的成功消息 在这种情况下 如果我按 F5 或刷新页面 它将触发按钮单击事件 为什么应该是这样 如何避免这种情况 When the
  • 如何在Python中检查字符串是否只包含数字或“/”?

    我正在尝试检查字符串是否仅包含数字或 以用作验证形式 但是我找不到同时执行这两项操作的方法 自动取款机我有这个 if variable isdigit False 这适用于数字 但我还没有找到一种方法来检查斜杠 有很多选项 如此处所示 列表
  • 通过 unixODBC 和 FreeTDS 从 MSSQL 返回西里尔字母的问题

    我在远程主机上的 Ubuntu 12 04 LTS 和 MSSQL 2008 上使用 django pyodbc 作为数据库后端 除了返回西里尔字母符号外 它的效果很好 我看到的不是它们 而是问号 我已经开始调查可能导致此问题的原因 据我了
  • java打印阶乘计算过程

    您好 我需要打印阶乘计算过程 例如 如果用户输入的是5 系统应该打印出 5 4 3 2 1 120 我有这个代码 public static void factorial Scanner sc new Scanner System in i
  • 带有绿色复选标记的控制台消息 JavaScript

    我想知道控制台中是否有可能有一个绿色复选标记 就像 console warn 有黄色警告标志 console error 有红色错误标志一样 我在网上搜索过是否有类似的功能 但没有找到 现在我正在寻找一种方法将图标放在console log
  • 为什么在计时器回调中调用事件会导致以下代码被忽略?

    我正在编写一个简单的游戏 使用来自system threading命名空间来模拟操作的等待时间 我的目标是让计时器每秒执行一次 持续 x 秒 为了实现这一点 我在计时器回调中添加了一个计数器 问题是我在调用后放置的任何代码DeliveryP