UI update with multiple concurrent operations

I am developing an application in C # using National Instruments Daqmx to perform measurements on specific hardware.

My setup consists of several detectors, from which I have to receive data for a given period of time, and all this time I update my interface with this data.

 public class APD : IDevice
 {
    // Some members and properties go here, removed for clarity.

    public event EventHandler ErrorOccurred;
    public event EventHandler NewCountsAvailable;

    // Constructor
    public APD(
        string __sBoardID,
        string __sPulseGenCtr,
        string __sPulseGenTimeBase,
        string __sPulseGenTrigger,
        string __sAPDTTLCounter,
        string __sAPDInputLine)
    {
       // Removed for clarity.
    }

    private void APDReadCallback(IAsyncResult __iaresResult)
    {
        try
        {
            if (this.m_daqtskRunningTask == __iaresResult.AsyncState)
            {
                // Get back the values read.
                UInt32[] _ui32Values = this.m_rdrCountReader.EndReadMultiSampleUInt32(__iaresResult);

                // Do some processing here!

                if (NewCountsAvailable != null)
                {
                    NewCountsAvailable(this, new EventArgs());
                }

                // Read again only if we did not yet read all pixels.
                if (this.m_dTotalCountsRead != this.m_iPixelsToRead)
                {
                    this.m_rdrCountReader.BeginReadMultiSampleUInt32(-1, this.m_acllbckCallback, this.m_daqtskAPDCount);
                }
                else
                {
                    // Removed for clarity.
                }
            }
        }
        catch (DaqException exception)
        {
            // Removed for clarity.
        }
    }


    private void SetupAPDCountAndTiming(double __dBinTimeMilisec, int __iSteps)
    {
        // Do some things to prepare hardware.
    }

    public void StartAPDAcquisition(double __dBinTimeMilisec, int __iSteps)
    {
        this.m_bIsDone = false;

        // Prepare all necessary tasks.
        this.SetupAPDCountAndTiming(__dBinTimeMilisec, __iSteps);

        // Removed for clarity.

        // Begin reading asynchronously on the task. We always read all available counts.
        this.m_rdrCountReader.BeginReadMultiSampleUInt32(-1, this.m_acllbckCallback, this.m_daqtskAPDCount); 
    }

    public void Stop()
    {
       // Removed for clarity. 
    }
}

      

The object representing the detector basically calls the BeginXXX operation with a callback that contains EndXXX en, also raises an event indicating the available data.

I have up to 4 of these detector objects as elements of my UI form. I call the Start () method on all of them in sequence to start the measurement. This works and the NewCountsAvailable event fires for all four.

Due to the nature of my implementation, the BeginXXX method is called on the UI thread, and the Callback and Event are also on that UI thread. So I cannot use some kind of while loop inside my UI thread to constantly update my UI with new data, because events are firing constantly (I tried this). I also don't want to use some sort of UpdateUI () method in each of the four NewCountsAvailable event handlers, as this will load my system too much.

Since I am new to C # programming, I am now stuck,

1) What is the "correct" way to handle this situation? 2) Does my implementation of the detector object sound? Should I call Start () methods on these four detection objects from another thread? 3) Can I use a timer to refresh my interface every few hundred milliseconds, no matter what the 4 detector objects are doing?

I really don't have a clue!

+2


source to share


5 answers


I would use a simple delayed update system.

1) Worker threads signal "data ready" by raising an event

2) The custom thread is listening for the event. When it is received, it simply sets the Update Data Needs flag and returns, so minimal processing happens in the event itself.



3) The UI thread uses a timer (or sits on Application.Idle events) to check the Update Data Needs checkbox and update the UI if needed. In many cases, the UI only needs to be refreshed once or twice a second, so it doesn't take a lot of CPU time.

This allows the UI to continue to work as usual all the time (remaining interactive to the user), but for a short period of time, some data is ready, it is displayed in the UI.

Additionally, and most importantly for a good user interface, this approach can be used to allow multiple “data preparation” events to fire and carry over into a single user interface update. This means that if 10 chunks of data are completed with tight sequencing, the UI will refresh once, rather than flickering for a few seconds when the UI is repeated (unnecessarily) 10 times.

+4


source


I would try to move the IDevice monitoring logic to split threads for each device. The user interface can then poll for values ​​using a timer event, button press, or other user interface related event. This way, your UI will stay responsive and your threads will do all the heavy lifting. Here's a basic example of this using a continuous loop. Obviously, this is a cruelly simple example.



public partial class Form1 : Form
{
    int count;
    Thread t = null;

    public Form1()
    {
        InitializeComponent();
    }
    private void ProcessLogic()
    {           
        //CPU intensive loop, if this were in the main thread
        //UI hangs...
        while (true)
        {
            count++;
        }
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        //Cannot directly call ProcessLogic, hangs UI thread.
        //ProcessLogic();

        //instead, run it in another thread and poll needed values
        //see button1_Click
        t = new Thread(ProcessLogic);
        t.Start();

    }
    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        t.Abort();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        button1.Text = count.ToString();
    }
}

      

+1


source


Some updates reflecting the new data you provided:

While I have doubts that your EndXXX methods are happening on the UI thread, I still think that you should defer work to a background thread and then update the interface either when events are fired or as needed.

Since you added a hard while loop to your UI, you need to call Application.DoEvents to allow other events to be fired.

Here's an updated sample that shows the results in the UI as they occur:

public class NewCountArgs : EventArgs
{
    public NewCountArgs(int count)
    {
         Count = count;
    }

    public int Count
    {
       get; protected set;
    }
}

public class ADP 
{
     public event EventHandler<NewCountArgs> NewCountsAvailable;

     private double _interval;
     private double _steps;
     private Thread _backgroundThread;

     public void StartAcquisition(double interval, double steps)
     {
          _interval = interval;
          _steps = steps;

          // other setup work

          _backgroundThread = new Thread(new ThreadStart(StartBackgroundWork));
          _backgroundThread.Start();
     }

     private void StartBackgroundWork()
     {
         // setup async calls on this thread
         m_rdrCountReader.BeginReadMultiSampleUInt32(-1, Callback, _steps);
     }

     private void Callback(IAsyncResult result)
     {
         int counts = 0;
         // read counts from result....

         // raise event for caller
         if (NewCountsAvailable != null)
         {
             NewCountsAvailable(this, new NewCountArgs(counts));
         }
     }
}

public class Form1 : Form
{
     private ADP _adp1;
     private TextBox txtOutput; // shows updates as they occur
     delegate void SetCountDelegate(int count);

     public Form1()
     {
         InitializeComponent(); // assume txtOutput initialized here
     }

     public void btnStart_Click(object sender, EventArgs e)
     {
          _adp1 = new ADP( .... );
          _adp1.NewCountsAvailable += NewCountsAvailable;
          _adp1.StartAcquisition(....);

          while(!_adp1.IsDone)
          {
              Thread.Sleep(100);

              // your NewCountsAvailable callbacks will queue up
              // and will need to be processed
              Application.DoEvents();
          }

          // final work here
     }

     // this event handler will be called from a background thread
     private void NewCountsAvailable(object sender, NewCountArgs newCounts)
     {
         // don't update the UI here, let a thread-aware method do it
         SetNewCounts(newCounts.Count);
     }

     private void SetNewCounts(int counts)
     {
         // if the current thread isn't the UI thread
         if (txtOutput.IsInvokeRequired)
         {
            // create a delegate for this method and push it to the UI thread
            SetCountDelegate d = new SetCountDelegate(SetNewCounts);
            this.Invoke(d, new object[] { counts });  
         }
         else
         {
            // update the UI
            txtOutput.Text += String.Format("{0} - Count Value: {1}", DateTime.Now, counts);
         }
     }
}

      

+1


source


I don't know if I fully understand. What if you update the object containing the current data. Thus, the callback does not directly interact with the UI. Then you can update the UI at a fixed rate for example. n times per second from another thread. See this post about updating the UI from a background thread . I am assuming you are using Windows Forms and not WPF.

0


source


The B * * * dy captcha system decided it would be nice to lose my answer. I spent half an hour typing without warning or a chance to fix ... so we over and over again:

public class APD : IDevice
 {
    // Some members and properties go here, removed for clarity.

    public event EventHandler ErrorOccurred;
    public event EventHandler NewCountsAvailable;

    public UInt32[] BufferedCounts
    {
        // Get for the _ui32Values returned by the EndReadMultiSampleUInt32() 
        // after they were appended to a list. BufferdCounts therefore supplies 
        // all values read during the experiment.
    } 

    public bool IsDone
    {
        // This gets set when a preset number of counts is read by the hardware or when
        // Stop() is called.
    }

    // Constructor
    public APD( some parameters )
    {
       // Removed for clarity.
    }

    private void APDReadCallback(IAsyncResult __iaresResult)
    {
        try
        {
            if (this.m_daqtskRunningTask == __iaresResult.AsyncState)
            {
                // Get back the values read.
                UInt32[] _ui32Values = this.m_rdrCountReader.EndReadMultiSampleUInt32(__iaresResult);

                // Do some processing here!

                if (NewCountsAvailable != null)
                {
                    NewCountsAvailable(this, new EventArgs());
                }

                // Read again only if we did not yet read all pixels.
                if (this.m_dTotalCountsRead != this.m_iPixelsToRead)
                {
                    this.m_rdrCountReader.BeginReadMultiSampleUInt32(-1, this.m_acllbckCallback, this.m_daqtskAPDCount);
                }
                else
                {
                    // Removed for clarity.
                }
            }
        }
        catch (DaqException exception)
        {
            // Removed for clarity.
        }
    }


    private void SetupAPDCountAndTiming(double __dBinTimeMilisec, int __iSteps)
    {
        // Do some things to prepare hardware.
    }

    public void StartAPDAcquisition(double __dBinTimeMilisec, int __iSteps)
    {
        this.m_bIsDone = false;

        // Prepare all necessary tasks.
        this.SetupAPDCountAndTiming(__dBinTimeMilisec, __iSteps);

        // Removed for clarity.

        // Begin reading asynchronously on the task. We always read all available counts.
        this.m_rdrCountReader.BeginReadMultiSampleUInt32(-1, this.m_acllbckCallback, this.m_daqtskAPDCount); 
    }

    public void Stop()
    {
       // Removed for clarity. 
    }
}

      

Note. I've added some things that I mistakenly left in the original post.

Now in my form I have code like this:

public partial class Form1 : Form
{
    private APD m_APD1;
    private APD m_APD2;
    private APD m_APD3;
    private APD m_APD4;
    private DataDocument m_Document;

    public Form1()
    {
        InitializeComponent();
    }

    private void Button1_Click()
    {           
        this.m_APD1 = new APD( ... ); // times four for all APD's

        this.m_APD1.NewCountsAvailable += new EventHandler(m_APD1_NewCountsAvailable);     // times 4 again...   

        this.m_APD1.StartAPDAcquisition( ... );
        this.m_APD2.StartAPDAcquisition( ... );
        this.m_APD3.StartAPDAcquisition( ... );
        this.m_APD4.StartAPDAcquisition( ... );

        while (!this.m_APD1.IsDone) // Actually I have to check all 4
        {
             Thread.Sleep(200);
             UpdateUI();
        }

        // Some more code after the measurement is done.
    }

    private void m_APD1_NewCountsAvailable(object sender, EventArgs e)
    {
        this.m_document.Append(this.m_APD1.BufferedCounts);

    }

    private void UpdateUI()
    {
        // use the data contained in this.m_Document to fill the UI.
    } 
}

      

phew, I hope I don’t forget anything to repeat it a second time (this will teach me not to copy it before hitting the post).

What I can see by running this code is:

1) The APD object works as advertised, it measures. 2) NewCountsAvailable events are triggered and their handlers are executed 3) APD.StartAPDAcquisition () is called on the UI thread. Thus, BeginXXX is called on this thread. So by design, the callback is on this thread as well, and obviously the NewCountsAvailable event handlers on the UI thread are also executed. The only thing not on the UI thread is waiting for the hardware to return values ​​in the BeginXXX EndXXX call pair. 4) Since NewCountsAvailable events are firing so many things, the while loop that I was planning to use to update the UI does not fire. It is usually fired once at the beginning and then processed in some way by the event handlers,to be processed. I don't quite understand this, but it doesn't work ...

I solved this problem by getting rid of the while loop and putting Forms.Timer on the form where UpdateUI () would be called from the Tick event handler. However, I don't know if this would be considered "best practice". I also don't know if all of these event handlers will end up crawling the UI thread, I may need to add a few more such APD objects in the future. Additionally, UpdateUI () may contain heavier code to compute the image based on the values ​​in m_Document. So the tick event handler can also be a resource leak in the timer approach. In case I use this solution, I also need to have a Done event in my APD class to notify me when each APD is complete.

Should I possibly not work with events to notify that new counts are available, but instead work with some sort of "on demand" reading of APD.BufferedCounts and put the whole thing on another thread? I really don't have a clue ...

I basically want a clean, lightweight solution that scales well if I add even more APDs :)

0


source







All Articles