I've made a chain of 4 producer-consumer threads (forming a 4 step pipeline). To my amazement, all four threads are running in sequence, instead of concurrently!!! That is, the second thread mysteriously waits until the first thread is entirely done producing. The third thread mysteriously waits until the second thread is entirely done producing (and so on).
It gets worse. If I put a Thread.Sleep(300) into the loop of the first producer, then the other three threads become concurrent and actually get processor time, as expected, producing "random interleaved" console output as expected from a multi-threaded app. I'm almost unable to accept the idea that a "sleep" is a necessary part of the solution, and yet I see that a sleep is incorporated in exactly that fashion in code written by Jon Skeet.
Please tell me that is not necessary to achieve concurrency, or if it is, then why?
A more precise story about my particular producer-consumer chain looks like this:
- First thread: in a tight loop it produces "widget" messages as fast as possible, pushing them into a queue for the next thread. A System.Threading.Timer is set for ~100 milliseconds when the first widget is added to the queue. The callback fired from the timer is the second thread...
- Second thread (fired from timer): reads some or all of the widgets from the prior queue. It sends them into another queue (to be consumed by the third thread). The monitor.Pulse/Wait mechanism is used to synchronize with the third thread.
- Third thread: Blocks on a monitor.Wait until monitor.Pulse is called, then fetches one item from the queue. The one item is pushed into the final queue, again using monitor.Pulse when the push is done.
- fourth thread: Blocks on a monitor.Wait until monitor.Pulse is called. Widgets are processed.
To process 1 million widgets through this pipeline takes about 4 minutes. In 4 minutes, there is PLENTY of time for the last 3 threads to be scheduled and do some work concurrently with the first thread. But as I said, the last three threads run in sequence, unless I introduce a tiny sleep for the first thread. It makes no sense.
Any thoughts on why this works this way?
p.s. Please do not tell me that a long producer-consumer chain, as I've described, can be shrunk or eliminated. Please trust me (or assume) that I need a chain that long. :)