Philosophical/design question...thoughts solicited

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • SoonerDave
    New Member
    • Aug 2010
    • 1

    #1

    Philosophical/design question...thoughts solicited

    Greetings, all. I have a philosophical/design question for you.

    Consider a fairly typical scenario involving a producer/consumer pattern. Now, consider two possible ways of implementing such a pattern in a C-Sharp/.NET Windows Forms application. Presume, for the sake of the discussion framework, the GUI spins up a producer on one thread, and the consumer on the other. As a final constraint on the concept, presume that the Consumer will update the GUI with information from the Producer items. Let's assume further, for the sake of discussion, that proper accomodations will be made in the Consumer-GUI interaction to ensure context switches to the GUI thread when such updates take place.

    Now, with that laid out, here are two possible design options regarding the production and consumption of items:

    1. The producer could notify the creation of its items of interest via explicitly raised events (exposing appropriate EventHandler delegates), with the consumer subscribing to the events via an appropriate event handler.

    2. The producer could, alternatively, add its items to a Queue object. The Queue would be shared between the producer and consumer, with the consumer polling on the queue until items are present to be consumed. Presume all checking on the queue would be wrapped by appropriate lock blocks for thread safety.

    I consider pros and cons for each model. My instinct suggests that while the event-based model is probably "purer" from an OO perspective (primarily because there's absolutely no coupling to its caller), my trick knee tells me that the complexity of this solution will start to creep up, particularly if/when the need to implement event asynchronous event handlers arises. I also suspect that the complexity could creep up if it becomes necessary to raise discrete events for each type of item produced. Those combine to suggest to me that there's a risk of the consumer falling behind if the work of consumption starts to take more than a trivial amount of time.

    Conversely, we have the "shared queue" model. Clearly, it has stronger coupling through the shared queue. But it does provide for what at least seems to me to be an opportunity for a fairly lightweight polling model - locking the shared queue only when checking for the presence of items, or to enqueue/dequeue items. This model also eliminates what I described earlier as possible "complexity creep" in the event-based model, because the need to raise explicit events as items are produced in this model is eliminated - the producer items are queued, and the consumer consumes them. The producer is also freed to continue producing nearly as rapidly or frequently as it may need, not being burdened with invoking event handlers and thus necessarily relieved of the issue of whether such handlers need to be asychronous. One could call this a "fire and forget" implementation.

    With that baseline, what are some other opinions/thoughts on these two possible implementations that I'm possibly overlooking? Is there a clear reason why either should be avoided or preferred?

    I realize some of the conclusions would depend upon implementation details I've not provided, hence the warning that the discussion is more philosophical or conceptual than literal at this point.

    Thanks in advance.
    SoonerDave
  • Aimee Bailey
    Recognized Expert New Member
    • Apr 2010
    • 197

    #2
    I know this is a very short reply, and I apologies giving that you have described your query in such a descriptive manner, but I thought id throw my proverbial foot into the mix first.

    I've seen this question debated about through many forums over the past couple of years, and one thing that has always stood out, has been the emphasis on choosing a model that tries to make best use of the technology made available, rather than re-inventing the wheel so to speak by developing a locking system. Even though the latter choice offers a direct method of tuning performance, I believe that it inevitably creates overhead.

    If I can muster up anything else to add, I'll surely let you know :) though this is quite an interesting one, so i hope other people add their opinion into the mix.

    Aimee.

    Comment

    • Christian Binder
      Recognized Expert New Member
      • Jan 2008
      • 218

      #3
      Standard Windows applications are event-driven and are using the Windows Message Queue.
      If I had to create such a producer-consumer-program, I'd prefer using an event-based queue, so if something is enqueued, the consumer is notified and it can request the enqueued item. So you don't have the necessity to poll the queue, what in general isn't the best way (polling).

      But it's also depending on how many subscribers there are, so it could be bad, if the first enqueues an item and the second one is missing it out, otherwise if no one removes items from the queue it will overflow.

      For your purpose (having one consumer, one producer, program is running as a single process, producer and consumer are just different threads), I'd use a synchronized queue, which sends events if a new item is enqueued. Further, the queue's events should be asyncronous, so that the producer isn't blocked by consumer-sided long-running operations.

      Comment

      • balabaster
        Recognized Expert Contributor
        • Mar 2007
        • 798

        #4
        I'd combine the two approaches. In C# there's a ManualResetEven t, have your consumer WaitAny. When the producer adds items to the queue, have your consumer loop through the queue until there's no more items left in the queue. When it's finished processing the queue, have it reset the ManualResetEven t. When the producer adds the next item(s), it trips the ManualResetEven t, telling the producer there's items in the queue to process.

        This way, your consumer avoids polling, it just sits and waits for the reset event to be tripped allowing it to continue processing, but it still processes everything available without having to wait for the reset event to be tripped per item.

        The reset event only really has any effect when the consumer is waiting to process items in the queue.

        Let's say the producer is capable of adding items to the queue faster than the consumer is capable of processing them - the consumer only ever sits and waits when there's no items left in the queue to process.

        The only lock on the queue you need is right at the end of processing wrapping to check if there's any items left and setting the wait flag, thus reducing the lock to the bare minimum time. The lock needs to be here because if the consumer notes that there's nothing left in the queue and then the producer adds an item and trips the event before the consumer can reset the event, then you get into a position where the consumer is going to sit idle until the producer adds another item while there was already an item in the queue to be processed.

        Consumer: Wait for manual reset event; Dequeue Item, Process Item; Lock queue; Check if there's any items remaining in the queue; If there are no items reset event; Unlock queue. Loop around to start...

        Producer: Lock queue (waiting for queue to become unlocked if necessary); Add an item to the queue; Trip reset event; Unlock queue.

        If the producer doesn't lock the queue ahead of adding the item, the consumer could finish processing the newly added item and reset the event before the producer has a chance to set the event, thus when the producer does set the event, it would trigger the consumer to attempt to process an empty queue.

        You can handle this in two ways - either have your consumer account for the possibility of being told to process a queue with zero items in it, or have your producer never tell your consumer to process an empty queue - or both. As a defensive coder, you never know when someone's going to come along and say "Oh, I know what would be a good idea, let's trip that ManualResetEven t from a different process", which causes your consumer to attempt to process a queue which is in an unknown state. So it's probably wise to assume that there will come a time when someone is going to tell your consumer to attempt to process an empty queue. If someone does that, then your consumer should gracefully handle that.

        When you're coding in multiple threads you have to consider that something else (including a whole other process) may occur on another thread between *every* line of code. That always needs to be your first and last thought on every call. If you stick to that, you'll be alright.

        Comment

        Working...