0

I am writing a WinForms application. I will have 2 threads, a UI thread and Operation thread. The UI thread produces Control events, and I want the Operation thread to consume them. The Operation thread is automated and will run Tasks for hardware control concurrently with the consumption of new events. Control events are but not limited to Start/Stop the automation

I am using BackgroundWorker because my Operation thread will also update the UI in the MVP architecture style.

Example of what I'd like, simplified for brevity.

public class UI : Form
{
    Operation _operation;
    BackgroundWorker _operationBackgroundWorker;
    
    public UI_Load(object sender, EventArgs e)
    {
        _operationBackgroundWorker.RunWorkerAsync();
    }
    
    private void operationBackgroundWorkerDoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker worker = sender as BackgroundWorker;
        _operation.DoWorkAsync(worker);
    }
    
    private void operationBackgroundWorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        // Only executed if something fails.
    }
    
    private void btnStart_Click(object sender, EventArgs e)
    {
        /* Much later in the Application lifecycle,
         * somehow trigger an event on operation thread
         * to begin operation.
         */
    }
}

/* The operation thread. This code must run as long as the 
 * application is open, or until a critical error occurs.
 */
public class Operation
{
    public async Task DoWorkAsync(BackgroundWorker worker)
    {
        // Live as long as the application and consume
        // user control events fired by UI thread.
        while (!worker.CancellationPending)
        {
            /* Make thread "sleep" until an event is triggered.
             * It should be possible to fire an event while another
             * operation is in progress such as stop or manual
             * override (a Queue). Operation thread also uses a
             * BackgroundWorker to execute hardware commands
             * to not block Event queue execution.
             */
             
             // Example operation
             foreach (var event in eventQueue)
             {
                switch (event)
                {
                    case Start:
                        // Run concurrently, for loop moves on
                        // to next event.
                        _automationWorker.RunWorkAsync();
                        // Uses Control.BeginInvoke() on controls
                        // to not block itself.
                        ControlView.ChangeControlToPause();
                    case Pause:
                        PauseAutomationWorker();
                        ControlView.ChangeControlToResume();
                    case Stop:
                        StopAutomationWorker();
                        ControlView.ChangeControlToStopped();
                }
             }
        }
    }
}

I have previously written my application with async/await and no separate Operation thread. That did not work because it made the codebase difficult to maintain and (I failed to find the bug) sometimes make the UI hang or not respond to new control inputs. Therefore I am looking for a cleaner alternative.

The_Matrix
  • 85
  • 7
  • 4
    So you are launching the `_operation.DoWorkAsync(worker);` operation in a fire-and-forget fashion. That's [not a good idea](https://stackoverflow.com/questions/36335345/web-api-fire-and-forget/36338190#36338190) in general. – Theodor Zoulias Aug 17 '23 at 20:17
  • @TheodorZoulias I gave a bad example, I'm sorry about that, but I do not want fire-and-forget. I want a separate `Operation` thread that is controlled by my `UI` thread. The `UI` thread will check on health of `Operation` thread and clean up after it. The only time my `Operation` thread closes on it's own is if hardware is not responding or fails, that is an `Exception`. Throughout the entire lifetime of the Application the `Operation` thread does work queued by the UI thread, but is not an independent entity. – The_Matrix Aug 17 '23 at 20:21
  • 4
    OK. The `BackgroundWorker` is totally unsuitable for this kind of work. I might post an answer with a better approach. – Theodor Zoulias Aug 17 '23 at 20:23
  • @TheodorZoulias Why not? I need my `Operation` thread to update `Controls` on the `UI` thread, it handles that well. I don't know if `Thread` can achieve that. – The_Matrix Aug 17 '23 at 20:25
  • 2
    Especially if there is hardware involved, you want to have a better feedback loop. That is your ui starts a request to change hardware state, then hardware driver must initiate that state change and report back when and if new state has been reached. Only then the UI should reflect the new state. Otherwise you _will_ get out of sync. – Fildor Aug 17 '23 at 20:37
  • @Fildor-standswithMods I see the issue now, thank you. I planned to remedy this with events such as `HardwareStartedMoving` or `HardwareStoppedMoving` when a `BackgroundWorker` command was finished, but on top of that `_automationWorker` I planned to also have a `_controllerWorker` inside that will concurrently call serial commands. – The_Matrix Aug 17 '23 at 20:43

1 Answers1

2

The BackgroundWorker invokes the DoWork event handler on the ThreadPool. Since your requirement is to have an "Operation thread", I assume that the thread should be dedicated to this purpose only, and so you should use the Thread constructor. As a conveyor belt for passing messages to the Operation thread from the UI thread, you can use a BlockingCollection<T>. Example:

_queue = new BlockingCollection<Command>();
_operationThread = new Thread(() =>
{
    foreach (Command command in _queue.GetConsumingEnumerable())
    {
        switch (command)
        {
            case Command.Start:
                // Do something
                break;
            case Command.Pause:
                // Do something else
                break;
        }
    }
})
{ IsBackground = true };
_operationThread.Start();

From the UI thread:

private void btnStart_Click(object sender, EventArgs e)
{
    _queue.Add(Command.Start);
}

In the FormClosing or FormClosed event of the form:

_queue.CompleteAdding();
_operationThread.Join();

Going the other way around, i.e. passing messages from the Operation thread back to the UI thread, is another story. You shouldn't manipulate UI controls directly from the Operation thread. You could use a Progress<T> instance, or the Control.Invoke/Control.BeginInvoke methods among other ways.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Am I able to wrap the `foreach (Command command in _queue.GetConsumingEnumerable())` in a `while (ConditionToKeepRunning)` loop and check the `_queue.Count` to see if I have any command to execute, otherwise the `Operation` thread continues to burns through the while loop? – The_Matrix Aug 17 '23 at 21:55
  • 1
    @The_Matrix no, you don't have to do that. The `BlockingCollection` is smart enough to block the thread efficiently while the collection is empty. No CPU is burned while the `foreach` loop is waiting for new commands to arrive. – Theodor Zoulias Aug 17 '23 at 21:58