In a context where thread affinity is required and a `SynchronizationContext` is not available (as in console applications or windows services), a blocking implementation is required. I have just a few recommendations: WaitHandle.WaitAny(new WaitHandle[] {killSubProc, channelWaitHandle}); This wait can cause an issue. If the remote application sends a message and while execution of xxx() continues on the main thread, another message is received, that new message will not be processed until another new message is received. Main Thread -> Wait xxx()----------------- WaitAny xxx() Thread X -> Message Set Thread Y -> Message Set Thread Z -> Message Set Another suggestion: > Move the queue and queueing/dequeueing logic into a class to de-couple it's implementation from invocation of XXX (Make a reusable blocking queue) Here we have a very generic Message class to store the callback handler and the payload (e.data in your case) internal class Message { private readonly SendOrPostCallback handler; private readonly object payload; internal Message(SendOrPostCallback handler, object payload) { this.handler = handler; this.payload = payload; } internal void Execute() { handler(payload); } } And another very short class for keeping track of the queue and to execute the operations (XXX in your case). Please check the use of `Queue.Synchronized` public class BlockingMessageSynchronizer { private Queue synchronizationQueue; private bool stopRequested; public void Start() { lock (this) { if (synchronizationQueue != null) { throw new InvalidOperationException("The synchronization has already started."); } synchronizationQueue = Queue.Synchronized(new Queue()); stopRequested = false; } while (!stopRequested) { // Thread.Sleep(1); ProcessQueue(); } ProcessQueue(); synchronizationQueue = null; } public void Post(SendOrPostCallback messageHandler, object payload) { if (synchronizationQueue == null || stopRequested) { lock (this) { if (synchronizationQueue == null || stopRequested) { throw new InvalidOperationException("The synhronization is not started or is stopped"); } } } synchronizationQueue.Enqueue(new Message(messageHandler, payload)); } public void Stop() { stopRequested = true; } private void ProcessQueue() { while (synchronizationQueue.Count > 0) { ((Message)synchronizationQueue.Dequeue()).Execute(); } } } This class can be used on ay thread to force execution of the callback handler on that thread, by blocking that thread. This implementation will cause 100% cpu since there is no Wait operations, you can consider `Thread.Sleep(1)` before the call to `ProcessQueue()` in the `while` loop in `Start()` And finally, your `stdout` or `stderr` handlers will look like: process.OutputDataReceived += (sender, e) => { if (e.Data != null) { synchronizer.Post(HandleCrossThreadMessage, e.Data); } }; process.ErrorDataReceived += (sender, e) => { if (e.Data != null) { synchronizer.Post(HandleCrossThreadMessage, "STDERR"); synchronizer.Post(HandleCrossThreadMessage, e.Data); } }; process.Exited += (sender, e) => { synchronizer.Post(HandleCrossThreadMessage, "EXIT"); synchronizer.Stop(); }; BlockingMessageSynchronizer synchronizer = new BlockingMessageSynchronizer(); process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); synchronizer.Start(); When you call `synchronizer.Stop();` inside the exit event, the remaining messages in the queue will be processed and the execution will get out of the `synchronizer.Start();` blocking call.