简体   繁体   中英

C# Updating UI from a multiple Tasks running in parallel

I have a problem regarding the UI and updating it from tasks. At trying to port my application to from winforms to UWP and in the process I wanted to optimize a CPU heavy part of the app.

Previously I used a backgroundworker to run the calculations, however using the Task API, I can increase the speed a lot. The problem arises when trying to update UI.

I'm doing a scan of a DNA strand for a number of 'features' that I have.

  • When a scan is started I want to update a label on the UI with the current 'task'.

  • When the scan is finished I want to send the 'size' of the feature so I can update the UI (progress bar and label) with the amount of data scanned.

  • And if the feature is found, I want to send it to the UI for display in a listview.

My current code works to some extent. It scans the DNA and finds the features and updates the UI. However, the UI freezes up a lot and sometimes it doesn't update more than a few times through the whole process.

I've searched the internet for a few days now in an attempt to solve my problem, but I can't figure out the best approach or if I should simply drop the Tasks and go back to a single backgroundworker.

So my question is what the right approach to this problem is.

How do I setup my tasks and report back to the UI thread in a reliable manner from multiple tasks at the same time?

I have written a code sample that resembles what my current setup is:

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, () => { });
    }
}

I hope my question is clear. Thank you.

Current solution and only solution I've been able to find so far Instead of launching 281 tasks at the same time, I launch 4 and await their finish:

        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;
                }
            }
        }

You can invoke a function back to the UI thread:

 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);

To apply this to your needs, have your tasks update a class or a queue, and then empty it into the UI using a single BeginInvoke.

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 - there may be some mis-spelling here. I have to leave like yesterday but I wanted to get my point across. I'll edit later.

EDIT for UWP, try

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

  });

in place of BeginInvoke;

From my experience Dispatcher.RunAsync is no a good solution when it can be raised often, because you cannot know when it will run.

You risks to add in dispatcher queue more works than the UI thread is able to perform.

Another solution is to create a thread-safe model shared between thread Tasks, and update the UI with a DispatcherTimer.

Here a sample sketch:

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
    }
}

If you want to run the same action for each element of the collection, I would go for Parallel.ForEach.

The trick is to use an IProgress<T> inside the ForEach code in to report updates to the main thread. The IProgress<T> constructor accepts an anonymous function that will be run in the main thread and can thus update the UI.

Quoting from 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);
  }
}

I created an IEnumerable<T> extension to run a parallel for with event callbacks that can directly modify the UI. You can have a look at it here:

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

Hope it helps!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM