c#从多个并行运行的任务中更新UI

本文关键字:任务 更新 UI 运行 并行 | 更新日期: 2023-09-27 18:07:44

我有一个关于UI和从任务更新它的问题。在尝试将我的应用程序从winforms移植到UWP时,在此过程中,我想优化应用程序中CPU繁重的部分。

以前我使用后台工作器来运行计算,但是使用Task API,我可以大大提高速度。当尝试更新UI时出现问题。

我正在扫描DNA链,寻找我的一些"特征"。

  • 当扫描开始时,我想用当前的'task'更新UI上的标签。

  • 当扫描完成时,我想发送功能的"大小",这样我就可以更新UI(进度条和标签)与扫描的数据量。

  • 如果功能被发现,我想把它发送到UI在listview中显示

我当前的代码在一定程度上工作。它扫描DNA,找到功能并更新UI。然而,UI经常冻结,有时在整个过程中只更新几次。

我已经在网上搜索了几天,试图解决我的问题,但我不知道最好的方法,或者我是否应该简单地放弃任务,回到一个单一的后台工作者。

所以我的问题是解决这个问题的正确方法是什么。

我如何设置我的任务,并报告回UI线程在一个可靠的方式从多个任务在同一时间?

我已经写了一个代码示例,类似于我目前的设置:

public class Analyzer
{
    public event EventHandler<string> ReportCurrent;
    public event EventHandler<double> ReportProgress;
    public event EventHandler<object> ReportObject;
    private List<int> QueryList; //List of things that need analysis
    public Analyzer()
    {
    }
    public void Start()
    {
        Scan();
    }
    private async void Scan()
    {
        List<Task> tasks = new List<Task>();
        foreach (int query in QueryList)
        {
            tasks.Add(Task.Run(() => ScanTask(query)));
        }
        await Task.WhenAll(tasks);
    }
    private void ScanTask(int query)
    {
        ReportCurrent?.Invoke(null, "name of item being scanned");
        bool matchfound = false;
        //Do work proportional with the square of 'query'. Values range from 
        //single digit to a few thousand
        //If run on a single thread completion time is around 10 second on     
        //an i5 processor
        if (matchfound)
        {
            ReportObject?.Invoke(null, query);
        }
        ReportProgress?.Invoke(null, query);
    }
}
public sealed partial class dna_analyze_page : Page
{
    Analyzer analyzer;
    private void button_click(object sender, RoutedEventArgs e)
    {
        analyzer = new Analyzer();
        analyzer.ReportProgress += new EventHandler<double>(OnUpdateProgress);
        analyzer.ReportCurrent += new EventHandler<string>(OnUpdateCurrent);
        analyzer.ReportObject += new EventHandler<object>(OnUpdateObject);
        analyzer.Start();
    }
    private async void OnUpdateProgress(object sender, double d)
    {
        //update value of UI element progressbar and a textblock ('label')
        //Commenting out all the content the eventhandlers solves the UI 
        //freezing problem
        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { /*actual code here*/});
    }
    private async void OnUpdateCurrent(object sender, string s)
    {
        //update value of UI element textblock.text = s
        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { });
    }
    private async void OnUpdateObject(object sender, object o)
    {
        //Add object to a list list that is bound to a listview
        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { });
    }
}
我希望我的问题是清楚的。谢谢你。

当前的解决方案和唯一的解决方案,我已经能够找到到目前为止我没有同时启动281个任务,而是启动4个并等待它们完成:

        List<Task> tasks = new List<Task>();
        for (int l = 0; l < QueryList.Count; l++)
        {
            Query query= QueryList[l];
            tasks.Add(Task.Run(() => { ScanTask(query); }, taskToken));
            //somenumber = number of tasks to run at the same time.
            //I'm currently using a number proportional to the number of logical processors
            if (l % somenumber == 0 || l == QueryList.Count + 1)
            {
                try
                {
                    await Task.WhenAll(tasks);
                }
                catch (OperationCanceledException)
                {
                    datamodel.Current = "Aborted";
                    endType = 1; //aborted
                    break;
                }
                catch
                {
                    datamodel.Current = "Error";
                    endType = 2; //error
                    break;
                }
            }
        }

c#从多个并行运行的任务中更新UI

你可以调用一个函数回UI线程:

 MethodInvoker mI = () => { 
     //this is from my code - it updates 3 textboxes and one progress bar. 
     //It's intended to show you how to insert different commands to be invoked - 
     //basically just like a method.  Change these to do what you want separated by semi-colon
     lbl_Bytes_Read.Text = io.kBytes_Read.ToString("N0");
     lbl_Bytes_Total.Text = io.total_KB.ToString("N0");
     lbl_Uncompressed_Bytes.Text = io.mem_Used.ToString("N0");
     pgb_Load_Progress.Value = (int)pct; 
 };
 BeginInvoke(mI);

要将此应用于您的需求,请让您的任务更新类或队列,然后使用单个BeginInvoke将其清空到UI中。

class UI_Update(){
public string TextBox1_Text {get;set;}
public int progressBar_Value = {get;set;}
//...

 System.ComponentModel.BackgroundWorker updater = new System.ComponentModel.BackgroundWorker();
public void initializeBackgroundWorker(){
    updater.DoWork += UI_Updater;
    updater.RunWorkerAsync();
}
public void UI_Updater(object sender, DoWorkEventArgs e){
   bool isRunning = true;
   while(isRunning){
      MethodInvoker mI = () => { 
      TextBox1.Text = TextBox1_Text; 
      myProgessBar.Value = progressBar.Value;
      };
      BeginInvoke(mI);
      System.Threading.Thread.Sleep(1000);
   }
 }
}

PS -这里可能有一些拼写错误。我不得不像昨天一样离开,但我想把我的观点说清楚。我稍后再编辑。

EDIT for UWP, try

CoreDispatcher dispatcher = CoreWindow.GetForCurrentThread().Dispatcher;
await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
  });

根据我的经验,Dispatcher。当RunAsync可以经常引发时,它不是一个好的解决方案,因为您无法知道它何时运行。

在dispatcher队列中添加的工作可能会超过UI线程的执行能力。

另一个解决方案是创建线程任务之间共享的线程安全模型,并使用DispatcherTimer更新UI。

这里是一个示例草图:

public sealed partial class dna_analyze_page : Page
{
    Analyzer analyzer;
    DispatcherTimer dispatcherTimer = null; //My dispatcher timer to update UI
    TimeSpan updatUITime = TimeSpan.FromMilliseconds(60); //I update UI every 60 milliseconds
    DataModel myDataModel = new DataModel(); //Your custom class to handle data (The class must be thread safe)
    public dna_analyze_page(){
        this.InitializeComponent();
        dispatcherTimer = new DispatcherTimer(); //Initilialize the dispatcher
        dispatcherTimer.Interval = updatUITime;
        dispatcherTimer.Tick += DispatcherTimer_Tick; //Update UI
    }
   protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);
        this.dispatcherTimer.Start(); //Start dispatcher
    }
   protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
    {
        base.OnNavigatingFrom(e);
        this.dispatcherTimer.Stop(); //Stop dispatcher
    }
   private void DispatcherTimer_Tick(object sender, object e)
    {
       //Update the UI
       myDataModel.getProgress()//Get progess data and update the progressbar
//etc...

     }
    private void button_click(object sender, RoutedEventArgs e)
    {
        analyzer = new Analyzer();
        analyzer.ReportProgress += new EventHandler<double>(OnUpdateProgress);
        analyzer.ReportCurrent += new EventHandler<string>(OnUpdateCurrent);
        analyzer.ReportObject += new EventHandler<object>(OnUpdateObject);
        analyzer.Start();
    }
    private async void OnUpdateProgress(object sender, double d)
    {
        //update value of UI element progressbar and a textblock ('label')
        //Commenting out all the content the eventhandlers solves the UI 
        //freezing problem
        myDataModel.updateProgress(d); //Update the progress data
    }
    private async void OnUpdateCurrent(object sender, string s)
    {
        //update value of UI element textblock.text = s
        myDataModel.updateText(s); //Update the text data
    }
    private async void OnUpdateObject(object sender, object o)
    {
        //Add object to a list list that is bound to a listview
        myDataModel.updateList(o); //Update the list data
    }
}

如果您想对集合中的每个元素运行相同的操作,我会选择Parallel.ForEach。

技巧是在ForEach代码中使用IProgress<T>来向主线程报告更新。IProgress<T>构造函数接受一个匿名函数,该函数将在主线程中运行,因此可以更新UI。

引自https://blog.stephencleary.com/2012/02/reporting-progress-from-async-tasks.html:

public async void StartProcessingButton_Click(object sender, EventArgs e)
{
  // The Progress<T> constructor captures our UI context,
  //  so the lambda will be run on the UI thread.
  var progress = new Progress<int>(percent =>
  {
    textBox1.Text = percent + "%";
  });
  // DoProcessing is run on the thread pool.
  await Task.Run(() => DoProcessing(progress));
  textBox1.Text = "Done!";
}
public void DoProcessing(IProgress<int> progress)
{
  for (int i = 0; i != 100; ++i)
  {
    Thread.Sleep(100); // CPU-bound work
    if (progress != null)
      progress.Report(i);
  }
}

我创建了一个IEnumerable<T>扩展来运行一个并行的事件回调,可以直接修改UI。你可以在这里查看:

https://github.com/jotaelesalinas/csharp-forallp

希望有帮助!