Почему вызов события в обратном вызове таймера вызывает игнорирование следующего кода? - PullRequest
1 голос
/ 29 февраля 2020

Я пишу простую игру, в которой используются таймеры из пространства имен system.threading для имитации времени ожидания действий. Моя цель - запускать таймер раз в секунду в течение x секунд. Чтобы добиться этого, я добавил счетчик в обратный вызов таймера.

Проблема в том, что любой код, который я размещаю после вызова события DeliveryProgressChangedEvent, кажется, игнорируется. Мой счетчик никогда не увеличивается, что позволяет таймеру работать вечно.

Если я вызываю событие после увеличения счетчика, все работает нормально. Просто ничего после вызова события не выполнится. Вместо того, чтобы идти по простому маршруту, который я хотел бы понять, если не решу эту проблему.

Я провел немало исследований по объекту system.threading timer, а также по событиям, но не смог найти ни одного информация, связанная с моей проблемой.

Я создал голый пример своего проекта, чтобы продемонстрировать приведенную ниже проблему.

Класс игры

    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 запускается.

1 Ответ

3 голосов
/ 29 февраля 2020

Проблема :
Использование класса System.Threading.Timer , когда при вызове TimerCallback возникают события, чтобы уведомить подписчиков DeliveryProgressChangedEvent и DeliveryCompletedEvent пользовательского Game класса хода выполнения процедуры и ее завершения.

В образце класса подписчик (класс Form, здесь) обновляет пользовательский интерфейс, устанавливает значение элемента управления ProgressBar, а также показывает MessageBox (используется в фактической реализации примера класса, показанного здесь).

Похоже, что после вызова первого события:

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

строка, где должно быть увеличено значение _counter, никогда не достигается, таким образом, код, который проверяет _counter для установки Таймер для нового значения никогда не выполняется.

Что происходит :

  1. System.Threading.Timer обслуживается потоками ThreadPool (более одного). Его обратный вызов вызывается в потоке, отличном от потока пользовательского интерфейса. События, вызываемые из обратного вызова, также генерируются в потоке ThreadPool.
    Код в делегате обработчика onDeliveryProgressChanged затем запускается в том же потоке.

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

    Когда отображается MessageBox - это модальное окно - оно блокирует поток, с которого он работает, как обычно. Код, следующий за строкой, в которой вызывается событие, никогда не достигается, поэтому _counter никогда не увеличивается:

    DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
    ++_counter;
    
  2. System.Threading.Timer может обслуживаться более чем одним потоком. Я просто цитирую Документы по этому вопросу, это довольно просто:

    Метод обратного вызова, выполняемый таймером, должен быть реентерабельным, потому что он вызывается в потоках ThreadPool. Обратный вызов может выполняться одновременно в двух потоках пула потоков, если интервал таймера меньше времени, необходимого для выполнения обратного вызова, или если все потоки пула потоков используются и обратный вызов ставится в очередь несколько раз.

    На практике происходит то, что хотя поток, в котором выполняется CallBack, блокируется MessageBox, это не останавливает Timer от выполнения CallBack из другого потока: нового MessageBox отображается, когда событие вызывается и продолжает работать до тех пор, пока у него не появятся ресурсы.

  3. У MessageBox нет владельца. Когда MessageBox отображается без указания владельца, его класс использует GetActiveWindow () , чтобы найти владельца окна MessageBox. Эта функция пытается вернуть дескриптор активного окна, присоединенного к очереди сообщений вызывающего потока. Но поток, из которого запускается MessageBox, не имеет активного окна, поэтому владельцем является рабочий стол (IntPtr.Zero).

    Это может быть проверено вручную , нажав на Откуда, где вызывается MessageBox: окно MessageBox исчезнет под формой, так как оно не принадлежит и не принадлежит .

Как решить :

  1. Конечно, используя другой таймер. System. Windows .Forms.Timer (WinForms) или DispatcherTimer (WPF) являются натуральными заменителями. Их события поднимаются в теме пользовательского интерфейса.

    ► Представленный здесь код является просто реализацией WinForms, созданной для воспроизведения проблемы, поэтому они могут применяться не ко всем контекстам.

  2. Использование System.Timers.Timer : свойство SynchronizingObject предоставляет средства для перенаправления событий обратно в поток, создавший текущий экземпляр класса (то же самое относится к конкретному контексту реализации).

  3. Создайте AsyncOperation , используя метод AsyncOperationManager.CreateOperation () , затем используйте делегат SendOrPostCallback , чтобы разрешить AsyncOperation вызовите метод SynchronizationContext.Post () (класс c стиль BackGroundWorker).
  4. BeginInvoke () MessageBox, присоединение к потоку пользовательского интерфейса SynchronizationContext. Например ,:

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

    Теперь MessageBox принадлежит Форме и будет вести себя как обычно. Поток ThreadPool можно продолжить: модальное окно синхронизируется с потоком пользовательского интерфейса.

  5. Избегайте использования MessageBox для такого рода уведомлений, так как это действительно раздражает :) Есть много других способов уведомить пользователя об изменениях статуса. MessageBox, вероятно, менее вдумчивый .

Чтобы заставить их работать как задумано, без изменения текущей реализации, классы Game и 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();

    private void MakeDelivery(object state)
    {
        DeliveryProgressChangedEvent?.Invoke(this, EventArgs.Empty);
        counter -= 1;

        if (counter == 0) {
            deliveryTimer?.Dispose();
            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);
            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();
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...