If you’ve ever tried to launch a new process in C#, you’ll probably have noticed that the
Process object lacks an asynchronous API. Luckily though, there is an easy way to write an asynchronous wrapper for a process such that you can launch it and
await its termination.
Getting to know Process
The first step in writing the wrapper is seeing how
Process works under normal circumstances. If you are writing code which spawns a process and writes to the console window when it finishes, you might do so as follows.
This code works, but it’s not terribly convenient to use. Let’s look at how we can make it
The key to our quest for asynchronicity is
TaskCompletionSource is a class which wraps a
Task whose state we can manually control. This is more easily understood with an example. Take a look at this console application.
When run, this code outputs “DoWork called”, pauses for approximately one second, and then outputs “6”. This behaviour is all due to the
TaskCompletionSource object that is created in the
TaskCompletionSource is first instantiated, the status of its underlying
Task object is set to
WaitingForActivation. In this state, any code which awaits the task will block. However, when
SetResult is called, the status of the task changes to
Completed and the task’s
Result property is set to the value passed to the
SetResult call. This causes threads waiting on the task to unblock and program execution to resume. In our example, the call to
Main blocks until
tcs.SetResult(6) is called by the task launched in
Calling a Process asynchronously
So, now that we’ve seen how a
TaskCompletionSource can be used to create a manually-controlled asynchronous method, we can apply the same logic to create an awaitable method that abstracts the behaviour of a
This method looks a lot like what we saw earlier. When called, it instantiates a new
TaskCompletionSource<object> and sets its result to
null when the process’s
Exited event is fired. Using this method, we can create a process and await its completion like so.
Needless to say, if we were passing anything meaningful to
tcs.SetResult, such as a value obtained from standard output, we could retrieve the value with a simple variable assignment.
Pretty cool if you ask me.
You may be wondering why we use a
TaskCompletionSource<object> and not just a
TaskCompletionSource. The reason is simply because a non-generic
TaskCompletionSource does not exist. Because in this case we have no return value to assign, we have simply opted to use a
TaskCompletionSource<object> and to set its result to
null upon completion.