views:

244

answers:

5

I am developing an application in C# using National Instruments Daqmx for performing measurements on certain hardware.

My setup consists of several detectors from which I have to get data during a set period of time, all the while updating my UI 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 a BeginXXX operation with a callback that holds the EndXXX en also fires an event indicating data available.

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

Due to the nature of my implementation, the BeginXXX method is called on the UI thread and the Callback and the Event are also on this UI thread. Therefore I cannot use some kind of while loop inside my UI thread to constantly update my UI with the new data because the events constantly fire (I tried this). I also do not want to use some kind of UpdateUI() method in each of the four NewCountsAvailable eventhandlers since this will load my system too much.

Since I am new to threaded programming in C# I am now stuck;

1) What is the "proper" way to handle a situation like this? 2) Is my implementation of the detector object sound? Should I call the Start() methods on these four detector objects from yet another thread? 3) Could I use a timer to update my UI every few hundred miliseconds, irrespective of what the 4 detector objects are doing?

I really have no clue!

A: 

I don't know if I fully understand. What if you update you an object that contains the current data. So the callback don't directly interact with the UI. Then you could update the UI at a fixed rate, e.g. n times per second from another thread. See this post on updating UI from a background thread. I am assuming that you are using Windows Forms and not WPF.

Steve
+1  A: 

I would try moving the IDevice monitoring logic to seperate threads for each device. The UI can then poll for values via a timer event, button click or some other UI related event. That way your UI will remain responsive and your threads are doing all the heavy lifting. Here's a basic example of this using a continuous loop. Obviously, this is a brutally 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();
    }
}
Steve
I was actually leaning towards a solution like this. I initially had four of my mentioned APD objects on my form. I would then call the APD.StartAPDAquisition() on each of them. Each of the 4 APD objects have their NewCountsAvailable events wired up to a method storing the counts coming from the APD in a document object. After the calls to APD.StartAPDAquisition() I had a while loop running that would update the UI every 200ms. However, since all these calls are on the UI thread, I noticed that the events firing would prevent the loop from running...
Kris
Since there are some things that need clarifying regarding my current code I added a more elaborate description below.
Kris
+1  A: 

Some updates to reflect the new data you've provided:

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

Because you've added a tight while loop in your UI, you need to call Application.DoEvents to allow your other events to be called.

Here's an updated sample that shows 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);
         }
     }
}
bryanbcook
I don't think I fully understand your suggestion. As things are now, my APD class already does stuff in another thread (reading the hardware) since the NI Daqmx API offers me the BeginRead and EndRead methods. This API also offers me a possibility to stop the hardware Task objects from which these BeginRead and EndRead calls get their data. I call this stop methods in my APD.Stop() method. My problem lies with having all these asynchronous operations doing their business and properly notifying the UI of progress. I added more code below to explain the problem further...
Kris
I've updated my answer based on the new info you've provided.
bryanbcook
<quote>Although I have my doubts that your EndXXX methods are happening on the UI thread</quote> -> As far as I could tell, they actually do. Maybe somebody could clarify this?
Kris
I will check your suggestions a bit later on. This morning I tried the solution of getting rid of the while loop and instead opting for a Timer that calls UpdateUI() when it ticks. UpdateUI() contains a call to DoEvents(). What I saw is that this seems to work as long as the rate of data coming in is not too high. If it was I'd run in to StackOverflow exceptions on the DoEvents(). Not sure what it all means yet but I'll be sure to test it all and post the final solution back here...
Kris
A: 

The B* * *dy captcha system decided it was a good idea to lose my answer I spent half an hour typing without so much as a warning or a chance to correct... so here we go 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 added some things I mistakenly left out in the original post.

Now on 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 dod not forget anything yping this a second time (that'll teach me not copying it before hitting Post).

What I see running this code is that;

1) The APD object works as advertised, it measures. 2) The NewCountsAvailable events fire and their handlers get executed 3) APD.StartAPDAcquisition() is called on the UI thread. Thus also BeginXXX is called on this thread. Therefore, by design, the callback is also on this thread and obviously also the NewCountsAvailable eventhandlers run on the UI thread. The only thing that is not on the UI thread is waiting for the hardware to return values to the BeginXXX EndXXX pair of calls. 4) Because the NewCountsAvailable events fire quite a lot, the while loop I intended to use for updating the UI does not run. Typically it runs once in the beginning and then somehow gets interupted by the eventhandlers that need to process. I do not fully understand this though, but it does not work...

I was thinking to solve this by getting rid of the while loop and putting a Forms.Timer on the form where UpdateUI() would be called from the Tick eventhandler. However, I do not know if this would be deemed "best practice". I also do not know if all these eventhandlers will eventually bring the UI thread to a crawl, I might need to add a few more of these APD objects in the future. Also UpdateUI() might contain some heavier code for calculating an image based on the values in m_Document. So the tick eventhandler might also be a resource drain in the timer approach. In case I use this solution I would also need to have a "Done" event in my APD class to notify when each APD finishes.

Should I perhaps not be working with events for notifying that new counts are available but instead work with some kind of "on demand" reading of APD.BufferedCounts and put the whole thing in yet another thread? I really haven't a clue...

I basically need a clean, lightweight solution that scales well should I add yet more APD's :)

Kris
+2  A: 

I'd use a simple deferred update system.

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

2) UI thread listens for the event. When it is received, it just sets a "data needs updating" flag and returns, so minimal processing occurs on the event itself.

3) UI thread uses a timer (or sits on Application.Idle events) to check the "data needs updating" flag and, if necessary, update the UI. In many cases, UI only needs to be updated once or twice a second, so this need not burn a lot of CPU time.

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

Additionally, and most importantly for good UI, this approach can be used to allow multiple "data ready" events to fire and be rolled into a single UI update. This means that if 10 pieces of data are completed in close succession, the UI updates once rather than your window flickering for several seconds as the UI redraws (unnecessarily) 10 times.

Jason Williams
Sounds reasonable. Being inexperienced with threading I was wondering -> If my APD object is running in a worker thread and it fires an event and the eventhandler is actually a method on my form class. In which thread will the eventhandler execute? And, if I want to set this flag like you said, will I have to use InvokeRequired calls to set this flag safely? Am I correct in assuming that the eventhandler will run from the worker thread?
Kris
Also, when my APD object has its StartAPDAcquisition() called in a worker thread and StartAPDAcquisition() relies on BeginXXX then I will actually have 3 threads? (1 UI thread, the Worker, the thread of the async operation) Is this correct and is this desirable?I could really use a good book on these topics :)
Kris
Event handlers are always called in the thread that raised the event (i.e. the worker thread in your case). To execute code in the UI thread, you can use an Invoke/BeginInvoke, or the deferred approach I've outlined above. (The deferred approach is very similar in effect to a BeginInvoke, except that multiple events can be merged into a single update)
Jason Williams