I have a habit of making several unrelated changes to projects around the same time. That works out fine in a controlled environment, like at work where there is source control, but it’s not so cool at home where I don’t have any source control installed, and I develop my “pet program”, used in most my examples here, which I also use as my code prototyping platform – and then later migrate pieces of it into work code.
Again, the source, for this and a whole bunch of other stuff, is here: RomyView.zip
Sometimes, some seemingly harmless code changes can introduce a subtle bug. That’s what happened here. It took me three days to find the problem because I was looking in the wrong place completely. I’ve probably given the game away in this post’s title, but anyway, do you see the bug in this code?
The above code performs a search for files, then asynchronously filters the results based on the search filter, using a ForEachAsync method that was posted on the Parallel Programming blog to populate a concurrent collection; then returns the collection’s results. (Notice it uses a ConcurrentBag<T>, so the results are unordered. In the bigger picture, there are also methods that return ordered results after performing a parallel sort, but that isn’t relevant to this post.)
The problem lies, as the title implies, in my use of the TaskCompletionSource<TResult> type. This is a very handy type, which allows us to implement some asynchronous code however we need, then “populate” and return a Task representing the asynchronous operation’s eventual completion.
But when using the TaskCompletionSource<TResult>, you need to make sure you set it’s Task that will be returned appropriately for all possible cases relevant to your particular code. I didn’t do that too well in the case above. Can you see what will happen if a search filter string is passed in, and there are any files that do not match?
The nested function is of type Func<string, Task<bool>>. That is, you pass in a filter string that may contain wildcards or may be empty if there is no search filter, and it returns a Task that indicates whether the file was added to the collection. When the calling code awaits the nested function, the compiler “lifts” the Task<bool> to a bool value, so the code can be treated like a synchronous function that had the signature Func<string, bool>. But I didn’t set the result for any unmatched files. Thus, if a search filter is passed in and any files don’t match it, this method will deadlock. That is, it will never return. If I hadn’t used a TaskCompletionSource<bool> (but instead marked the Func as async, such that I could just return a bool in the code), or if the function was synchronous, with a Func<string, bool> signature, the code would have resulted in a compiler error, bringing this to my attention.
In retrospect, this might seem pretty obvious, but I looked at the code for 3 days without finding the problem. The point is, if you use a TaskCompletionSource<TResult> type, make sure you set the return value for its underlying Task correctly. (Which might also involve handling Cancellation, which wasn’t necessary here.)
Here is the same code with the bug fixed: