winforms 跨线程操作和使用Invoke重复多次,最好的方法

bejyjqdl  于 2023-06-24  发布在  其他
关注(0)|答案(3)|浏览(185)

这是一个WinForms应用程序,其中有一个类,它有几个方法需要花费很多时间,这些方法以一定的时间间隔引发事件,我使用这些事件来更新GUI,更改标签,文本,颜色等。
任务运行在不同的线程上,所以它们不会阻塞UI,这很好,因为我要么从System.Timers.Timer调用它们,要么创建一个任务列表并异步运行它们。
我遇到的问题是,在我为那个类创建的报告我不同状态的事件处理程序上,由于跨线程操作,我无法更新GUI,因此我所做的解决方法是检查EventHandler上的InvokeRequired,然后检查BeginInvoke
这样做的技巧,但我肯定这是不正确的,我的EventHandler可以调用数百次,每次我开始一个新的Invoke。最好的方法是什么?我没有经验与代表,不知何故,我认为我应该使用他们,不知道如何
我在这里留下一段示例代码,它大大简化了工作负载和问题

public partial class Form1 : Form {
  private Operational o = new();
  private System.Timers.Timer WorkerTimer=new() { Interval=1000,Enabled=false};

  public Form1() {
    InitializeComponent();
    o.OperationComplete += OperationCompleteEventHandler;
    WorkerTimer.Elapsed += MonitorExecutor;
  }

  private async void MonitorExecutor(object? sender, ElapsedEventArgs e) {
    List<Task> myTasks = new();
    myTasks.Add(Task.Run(o.DoWork));
    myTasks.Add(Task.Run(o.DoWork));
    await Task.WhenAll(myTasks);
    // Report all tasks have completed
  }

  private void OperationCompleteEventHandler(object? sender, EventArgs e) {
    if (InvokeRequired) {
      // Otherwise it will throw a cross-thread exception
      Debug.WriteLine("Invoked Required!");
      BeginInvoke(() => OperationCompleteEventHandler(sender, e));
      return;
    }
    label1.Text = "WorkCompleted";
    // But this could also take a lot of time,
    // so I don't want this method to hang my thread
    Thread.Sleep(500);
  }

  private void button1_Click(object sender, EventArgs e) {
    WorkerTimer.Enabled = !WorkerTimer.Enabled;
    button1.Text = WorkerTimer.Enabled ? "Running" : "Stopped";
  } 
}

public class Operational {
  public event EventHandler? OperationComplete;

  public void DoWork() {
    // Long Operations
    Thread.Sleep(500);
    OperationComplete?.Invoke(this, new());
  }

}
vuv7lop3

vuv7lop31#

你没有正确使用await。假设WinForms设置了一个SynchronizationContext,那么您应该依靠它来将延续(await之后的位)封送到正确的线程上。
有许多不同的方法可以实现这一点,但主要需要将OperationComplete的调用移动到普通的await,并使用Task.Run运行其余代码。

public async Task DoWork()
{
    await Task.Run(DoWorkCore);
    OperationComplete?.Invoke(this, new());
}

private void DoWorkCore()
{
    // Long Operations
    Thread.Sleep(500);
}

然后您可以删除整个if (InvokeRequired) {块,因为现在OperationCompleteEventHandler将始终在UI线程上运行。

private async void OperationCompleteEventHandler(object? sender, EventArgs e)
{
    label1.Text = "WorkCompleted";
    await Task.Run(() => {
        // some other long running stuff
        Thread.Sleep(500);
    });
    // more UI stuff here
}

MonitorExecutor可以简单地

private async void MonitorExecutor(object? sender, ElapsedEventArgs e)
{
    List<Task> myTasks = new();
    myTasks.Add(o.DoWork);
    myTasks.Add(o.DoWork);
    await Task.WhenAll(myTasks);
    // Report all tasks have completed
}

如果不想更改DoWork,则需要使用某种类型的事件到任务转换,例如A reusable pattern to convert event into taskGeneral purpose FromEvent method

snvhrwxg

snvhrwxg2#

我建议不要使用System.Timers.Timer组件。这不是一个设计良好的组件IMHO。你可以在这个答案的底部阅读我的论点。
要定期执行工作而不存在重叠执行的风险,可以使用async方法和PeriodicTimer组件。有关使用示例,请参阅此答案。
至于每秒调用UI线程数百次,这不是一个好主意,因为UI线程负担过重的风险,使您的应用程序无响应,特别是如果它运行在慢速机器上。您应该将更新UI的频率降低到每秒最多10 - 20次更新。比这更频繁地更新UI是没有意义的,因为这超出了人类处理变化信息的视觉能力。为了限制信息流,您可以使用带有Debounce/Throttle运算符的Rx等工具,或者使用使用Stopwatch es和TimeSpan s的自定义代码来实现相同的效果,或者使用递增的计数器并仅在++counter % 10 == 0等时更新UI。

q35jwt9p

q35jwt9p3#

如果您愿意使用其他方法来做到这一点,这里有一个使用Microsoft的Reactive Framework(又名Rx)的选项- NuGet System.Reactive.Windows.Forms并添加using System.Reactive.Linq;-然后您可以这样做:

public partial class Form1 : Form
{
    private Operational o = new();

    public Form1()
    {
        InitializeComponent();
        Observable
            .Interval(TimeSpan.FromSeconds(2.0))
            .ObserveOn(this)
            .Do(x => label1.Text = "WorkStarted")
            .SelectMany(x => Observable.FromAsync(() => o.DoWork()))
            .SelectMany(x => Observable.FromAsync(() => o.DoWork()))
            .ObserveOn(this)
            .Subscribe(x => label1.Text = "WorkCompleted");
    }
}

public class Operational
{
    public async Task DoWork()
    {
        await Task.Delay(TimeSpan.FromMilliseconds(250.0));
    }
}

它可能更简洁一点,可能更容易推理。

相关问题