【问题标题】:ISynchronizeInvoke Invoke vs BeginInvokeISynchronizeInvoke 调用与 BeginInvoke
【发布时间】:2021-04-28 00:17:59
【问题描述】:

我已经编写了自己的计时器类,它是Windows Multimedia Timers 的包装器。

我在 .NET 的 System.Timers.Timer 类上建模了我的计时器类。

我很难理解为什么 .NET 的计时器在同步对象上调用 BeginInvoke,而不是 Invoke

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

我的理解是 BeginInvoke 最终应该通过调用 EndInvoke 来匹配。找不到EndInvoke

  1. .NET 的做法有什么“错误”吗?为什么BeginInvoke 在这里更可取?

  2. 如果我也在课堂上使用BeginInvoke,这是否意味着我的课堂的计时器事件可能会在前一个事件完成之前触发(重新进入问题)?这不会破坏(某些)同步到同步对象的目的吗?

【问题讨论】:

    标签: c# .net asynchronous events delegates


    【解决方案1】:

    Invoke 方法会阻塞调用线程,直到提供的委托的执行完成为止,由于各种原因,这可能需要很长时间。例如,委托可能包含阻塞调用,或者目标上下文可能被暂时阻塞等。System.Timers.Timer 类的调用线程始终是ThreadPool 线程,这是一个有限资源池。阻塞ThreadPool 线程是个坏主意,因为它很容易导致池饱和,导致效率低下并降低响应能力。这就是为什么调用BeginInvoke 更可取的原因,因为它只是调度委托的执行,而调度通常是一个非常快的操作。不需要调用EndInvoke

    System.Timers.Timer.Elapsed 事件在设计上是可重入的,如果它调用 Invoke 而不是 BeginInvoke,它仍然是可重入的。那是因为事件是在线程池上触发的。相比之下,System.Windows.Forms.Timer.Tick 事件是不可重入的,因为它总是在同一个线程(即 UI 线程)上触发。 System.Timers.Timer.Elapsed 事件的可重入性是不使用 System.Timers.Timer 类,而是使用异步循环的论据。你可以看看这个问题的例子:Run async method regularly with specified interval。不使用此类的其他参数是事件处理程序swallows exceptions,并且该类不是线程安全的。线程安全的类一般不会公开事件,因为事件是inherently not-thread-safe mechanism

    【讨论】:

    • 你已经澄清了这么多,谢谢。您能否澄清一件事 - 您提到“BeginInvoke 安排委托的执行”,什么机制处理调度?特别是,如果多个 BeginInvokes 调度委托的速度快于委托方法执行其代码的速度,会发生什么情况?呼叫是否以某种方式排队或 是我们遇到烦人的重入问题的地方?
    • @MichalCihelka 调度机制是特定ISynchronizeInvoke 实现的一部分。在实践中,99% 的时间将工作安排在启用 GUI 的应用程序的 UI 线程上,该线程维护一个内部调度委托队列。当这个队列被消息淹没时会发生什么,在这个问题中讨论:WinForms message loop not responsive
    • 哇。与谷歌搜索委托、事件和多线程的日子相比,我从这个答案中学到的更多。非常感谢您-您的回答和宝贵的链接。我现在看得更清楚了——理解 Windows 的底层消息循环架构是关键。现在,有了这些知识,我必须在 my 计时器中决定是 Invoke 还是 BeginInvoke。我希望在实践中它不会(或不应该)产生任何真正的影响。
    • 如果我可以扩展我的最后一句话,“我希望在实践中它不会(或不应该)产生任何真正的影响”。为了其他人的利益,在我的情况下它不会有太大区别的原因是因为我的计时器不打算在主 UI 线程上做任何事情。但是,如果我要使用计时器来更新表单上的控件,那么 BeginInvoke 似乎是更好/更安全的方法。
    【解决方案2】:

    Invoke 会阻塞当前线程和主线程。

    BeginInvoke 只阻塞主线程。

    你可以试试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);
            }
        }
    }
    

    【讨论】:

    • 我非常感谢您的努力,谢谢 :-) 我真正想要的是在提高时选择BeginInvoke 而不是Invoke含义更加清晰事件,以及 MS 在他们自己的引发 Elapsed 事件的计时器的回调代码中选择一个而不是另一个的原因。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-04-25
    • 2011-09-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多