Threading — Producer-Consumer Pattern

Hav­ing run into a bit of dead­lock­ing issue while work­ing on Bingo.Net I spent a bit of time read­ing into the Pro­duc­er-Con­sumer pat­tern and here’s what I learnt which I hope you’ll find use­ful too.

AutoResetEvent and ManualResetEvent

To start off, MSDN has an intro­duc­to­ry arti­cle on how to syn­chro­nize a pro­duc­er and a con­sumer thread. It’s a decent start­ing point, how­ev­er, as some of the com­ments point­ed out the sam­ple code is bug­gy and allows for a race con­di­tion to hap­pen when the AutoRe­setEvent is reset in quick suc­ces­sion whilst the con­sumer thread is pro­cess­ing the pre­vi­ous reset.

The prob­lem with the AutoRe­setEvent is that you can set an event that is already set which does not have any effect:

image

And just like that, the Con­sumer has missed one of the items! How­ev­er, you can work around this eas­i­ly enough by lock­ing onto the queue’s sync root object when it wakes up and dequeue as many item as there are on the queue. This might not always be applic­a­ble to your require­ment though.

A much bet­ter choice would be to use the Mon­i­tor class instead.

Using Monitor.Pulse and Monitor.PulseAll

As Jon Skeet point­ed out in his Thread­ing arti­cle (see ref­er­ence) the Auto/Man­u­al­Re­setEvent and Mutex class­es are sig­nif­i­cant­ly slow­er than Mon­i­tor.

Using the Monitor.Wait, Monitor.Pulse and Monitor.PulseAll meth­ods, you can allow mul­ti­ple threads to com­mu­ni­cate with each oth­er sim­i­lar to the way the reset events work but only safer. I have found quite a num­ber of dif­fer­ent imple­men­ta­tions of the Pro­duc­er-Con­sumer pat­tern using these two meth­ods, see the ref­er­ences sec­tion for details.

Whilst the imple­men­ta­tions might dif­fer and depend­ing on the sit­u­a­tion some­times you might want mul­ti-pro­duc­er/­con­sumer sup­port and oth­er times you might want to ensure there can only be one producer/consumer. What­ev­er your require­ment might be, the gen­er­al idea is that both producer(s) and consumer(s) have to acquire a lock on the same sync object before they can add or remove items from the queue. And depend­ing on who’s hold­ing the lock:

  • producer(s) would acquire a lock, add item to the queue, and then pulse, which gives up the lock and wait up oth­er wait­ing threads (the con­sumers)
  • consumer(s) would acquire a lock, start lis­ten­ing for items in a con­tin­u­ous loop and wait for a pulse, which gives up the lock (allow­ing oth­er producers/consumers to get in and acquire the lock). When the con­sumer is wok­en up it reac­quires the lock and can then safe­ly process new items from the queue.

There are a num­ber of oth­er con­sid­er­a­tions you should take into account, such as the exit con­di­tions of the consumer’s con­tin­u­ous loop, etc. Have a look at the dif­fer­ent imple­men­ta­tions I have includ­ed in the ref­er­ence sec­tion to get a feel of what you need to do to imple­ment the Pro­duc­er-Con­sumer pat­tern to suit your needs.

Parting thoughts..

Anoth­er thing which you should be aware of when imple­ment­ing any sort of Pro­duc­er-Con­sumer mod­el is the need to make the queue thread-safe see­ing as mul­ti­ple threads can be reading/writing to it con­cur­rent­ly. As you might know already, there is a Queue.Synchronized method which you can use to get a syn­chro­nized (in oth­er words, thread-safe) ver­sion of the queue object.

But as dis­cussed on the BCL Team Blog (see ref­er­ence), the syn­chro­nized wrap­per pat­tern gave devel­op­ers a false sense of secu­ri­ty and led some Microsoft devel­op­ers to write unsafe code like this:

if (syncQueue.Count > 0) {
    Object obj = null;
    try {
        // count could be changed by this point, hence invalidating the if check
        bj = syncQueue.Dequeue();
    }
    catch (Exception) { } // this swallows any indication we have a race condition
    // use obj here, dealing with null.
}

Which is why they decid­ed it’s bet­ter to force devel­op­ers to use explic­it lock state­ment around the whole oper­a­tion.

Anoth­er way to think about the prob­lem is to pic­ture the Queue.Synchronized method as the equiv­a­lent of a volatile mod­i­fi­er for a ref­er­ence type which guar­an­tees the lat­est val­ue of the instance (as opposed to the ref­er­ence point­er only). Which means it’s save to use in atom­ic oper­a­tions (a sin­gle instruc­tion) but does not stop inter­leav­ing instruc­tions from mul­ti­ple threads between oper­a­tions.

References:

MSDN arti­cle on how to syn­chro­nize a pro­duc­er and a con­sumer thread

Jon Skeet’s arti­cle on Wait­Handles

Jon Skeet’s arti­cle on Dead­locks and an imple­men­ta­tion of the Pro­duc­er-Con­sumer pat­tern using Mon­i­tor

Mike Kempf’s imple­men­ta­tion of the Pro­duc­er-Con­sumer pat­tern using Mon­i­tor

BCL Team Blog post on why there is not Queue<T>.Synchronized

Stack­Over­flow ques­tion on why there’s no Queue<T>.Synchronized