5
\$\begingroup\$

I had a need to isolate some long-running blocking calls in a background thread of my app. I also needed to keep this thread running indefinitely, because COM would complain if some object I created in that thread were accessed from another thread. I then needed to control what these objects did from my main UI thread, in a guaranteed-FIFO manner.

The solution I came up with was kind of ActiveObject-reminiscent; a polling loop running as the DoWork handler of a BackgroundWorker, which accepted encapsulated Command objects representing the work to perform from a thread-safe ConcurrentQueue. Here's the vitals:

//basic implementation of the Command pattern; no serialization/persistence needed,
//just need to be able to encapsulate some work and defer it.
internal class Command
{
    private Action action;

    public Command(Action action)
    {
        this.action = action;
    }

    protected Command()
    {
    }

    public virtual void Execute()
    {
        action();
    }
}

//Generic variation accepts a single parameter
internal class Command<T>:Command
{
    private T param;

    private Action<T> action;

    public Command(Action<T> action, T param)
    {
        this.action = action;
        this.param = param;
    }

    public override void Execute()
    {
        action(param);
    }
}

...

//event handlers run in the UI thread will Enqueue() Command objects
//containing the delegates that should be run in the background thread
ConcurrentQueue<Command> queuedCommands = new ConcurrentQueue<Command>();


//The BGW DoWork handler; runs indefinitely until the UI that needs it is Dispose()d
private void MainBackgroundLoop(object sender, System.ComponentModel.DoWorkEventArgs e)
    {
        Command command;
        bool commandAvailable;

        do
        {
            commandAvailable = queuedCommands.TryDequeue(out command);
            if (commandAvailable)
                command.Execute();
            else
                Thread.Sleep(100); //<-- Here's your code smell
        } while (!e.Cancel);
    }

Now, this functions beautifully. But, the use of Thread.Sleep() in any SO/CR post usually gets called out as a code smell. I get why; this loop requires CPU to try to pull commands out of the queue at least 10 times a second, which will eat CPU for an "idle" thread. But, given there are other reasons why the thread has to keep running, this seems acceptable.

Thoughts?

\$\endgroup\$
2
  • \$\begingroup\$ Shouldn't you be using delegates or Tasks? Or even events? \$\endgroup\$
    – Snowbody
    Commented Mar 8, 2012 at 17:34
  • \$\begingroup\$ I am using delegates. Action is a particular-system defined delegate type, as is Action<T>. I encapsulate these in Command objects because an Action<T> isn't an Action, and to avoid external closures (referencing variables that may, and in fact will, go out of scope before the delegate is evaluated). \$\endgroup\$
    – KeithS
    Commented Mar 8, 2012 at 18:02

1 Answer 1

15
\$\begingroup\$

You're right, most of the time, you shouldn't use polling like this. You should instead block the thread indefinitely if there is no work and wake it up when it's needed.

Fortunately, the same namespace that contains the ConcurrentQueue<T> you use also has the solution you need: BlockingCollection<T>. The collection has methods that will always return an item, and block if none are available, which is exactly what you need. Using that class, I would rewrite your code like this:

BlockingCollection<Command> queuedCommands = new BlockingCollection<Command>();

private void MainBackgroundLoop(object sender, DoWorkEventArgs e)
{
    do
    {
        Command command;
        bool commandAvailable = queuedCommands.TryTake(out command, Timeout.Infinite);
        if (commandAvailable)
            command.Execute();
    } while (commandAvailable);
}

Because the collection doesn't know about your DoWorkEventArgs, you have take care of ending the loop in some other way. In the code above, if you call queuedCommands.CompleteAdding(), it will make sure remaining items are processed and then it will shut down, because TryTake() will return false. Another option for cancellation is to use an overload of TryTake() that takes a CancellationToken and use that to cancel the operation.

\$\endgroup\$
5
  • \$\begingroup\$ That's beautiful; +1 for the built-in solution. Now, it will block my user code, but behind the scenes it will still use some polling mechanism to keep track of the timeout and/or cancellation. I guess I should trust that it's CPU-efficient, but "just because you can't see the implementation's complexity doesn't mean it's free". I'm guessing it uses threading with WaitHandles. \$\endgroup\$
    – KeithS
    Commented Mar 8, 2012 at 20:58
  • \$\begingroup\$ One more question; how's the FIFO nature of this? Order of operations can sometimes be important, so if I put in commands 1, 2, and 3, I need to get them back in that order. \$\endgroup\$
    – KeithS
    Commented Mar 8, 2012 at 21:14
  • \$\begingroup\$ Never mind the follow-up Q; I found on another answer that a BlockingCollection uses a ConcurrentQueue by default, and can be told to use any IProducerConsumerCollection implementation. So, I can specify once and for all that it uses a ConcurrentQueue, and TryTake will dequeue the items FIFO. \$\endgroup\$
    – KeithS
    Commented Mar 8, 2012 at 21:27
  • \$\begingroup\$ @KeithS, yes exactly, ConcurrentQueue<T> is used by default, but you can switch it if you want. \$\endgroup\$
    – svick
    Commented Mar 9, 2012 at 0:27
  • 2
    \$\begingroup\$ WOW, this simple example gave me a great start on a solution that's much more effective. I never understood the blocking nature of the BlockingCollection, and it was a perfect solution to getting rid of my Thread.Sleep message loop. Thanks a LOT!!! \$\endgroup\$
    – user13572
    Commented May 22, 2012 at 0:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.