Michal Cihelka
Michal Cihelka

Reputation: 297

ISynchronizeInvoke Invoke vs BeginInvoke

I have written my own timer class, a wrapper for Windows Multimedia Timers.

I modelled my timer class on .NET's System.Timers.Timer class.

I am struggling to understand why .NET's timer calls BeginInvoke on the synchronizing object, rather than Invoke:

if (this.SynchronizingObject != null && this.SynchronizingObject.InvokeRequired)
{
    this.SynchronizingObject.BeginInvoke(intervalElapsed, new object[]{this, elapsedEventArgs};
}
else
{
    intervalElapsed(this, elapsedEventArgs);
}

My understanding was that BeginInvoke should eventually be matched by a call to EndInvoke. There is no EndInvoke to be found.

  1. Is there anything "wrong" with the way .NET is doing it? Why might BeginInvoke be preferable here?

  2. If I use BeginInvoke in my class too, would that mean my class' timer event could fire before a previous event has completed (re-entrancy issues)? Wouldn't that defeat (some of) the purpose of synchronizing to a synchronizing object?

Upvotes: 1

Views: 548

Answers (2)

Theodor Zoulias
Theodor Zoulias

Reputation: 43384

The Invoke method blocks the calling thread until the completion of the execution of the supplied delegate, which may take a long amount of time, for various reasons. For example the delegate may contain blocking calls, or the target context may be temporarily blocked, etc. The calling thread in the case of the System.Timers.Timer class is always a ThreadPool thread, which is a pool of limited resources. Blocking a ThreadPool thread is a bad idea, because it can easily lead to the saturation of the pool, causing inefficiency and reduced responsiveness. That's why calling the BeginInvoke is preferable, because it just schedules the execution of the delegate, and scheduling is normally a pretty fast operation. Calling the EndInvoke is not required.

The System.Timers.Timer.Elapsed event is re-entrant by design, and it would still be re-entrant if it called the Invoke instead of the BeginInvoke. That's because the event is triggered on a pool of threads. For comparison the System.Windows.Forms.Timer.Tick event is not re-entrant, because it is triggered always on the same thread, the UI thread. The re-entrant nature of the System.Timers.Timer.Elapsed event is an argument for not using the System.Timers.Timer class, and using instead an asynchronous loop. You can look at this question for examples: Run async method regularly with specified interval. Other arguments for not using this class are that the event handler swallows exceptions, and that the class is not thread-safe. Thread-safe classes do not expose events in general, because an event is an inherently not-thread-safe mechanism.

Upvotes: 2

Allen Hu
Allen Hu

Reputation: 141

The Invoke will block current thread and main thread.

And BeginInvoke only block main thread.

you can try in wpf

MainWindow.xaml

<Window x:Class="WpfApp5.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp5"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
    </Window.Resources>
    <UniformGrid Columns="1">
        <TextBlock Text="{Binding TimeString}"/>
        <Button Content="Invoke" Click="Invoke_Button_Click"/>
        <Button Content="BeginInvoke" Click="BeginInvoke_Button_Click"/>
    </UniformGrid>
</Window>

MainWindow.xaml.cs

using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;

namespace WpfApp5
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, INotifyPropertyChanged
    {

        public event PropertyChangedEventHandler PropertyChanged;

        private string tmeString;
        public string TimeString
        {
            get { return this.tmeString; }
            set
            {
                this.tmeString = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TimeString)));
            }
        }

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
            Task.Run(() =>
            {
                while (true)
                {
                    TimeString = $"DateTimeNow : {DateTime.Now}";

                    Thread.Sleep(1000);
                }
            });
        }

        private void BeginInvoke_Button_Click(object sender, RoutedEventArgs e)
        {
            Dispatcher.BeginInvoke((Action)SomeWork, null);
            //break point here
            bool buttonClickEventEnd = true;
        }

        private void Invoke_Button_Click(object sender, RoutedEventArgs e)
        {
            Dispatcher.Invoke((Action)SomeWork, null);
            //break point here
            bool buttonClickEventEnd = true;
        }

        private void SomeWork()
        {
            Thread.Sleep(3 * 1000);
        }
    }
}

Upvotes: 1

Related Questions