TL;DR: Are exceptions OK to cancel innermost calls? I want my code to be both maintainable and fast. But maintainability is the most important.
In my first approach I avoided exception based canceling as much as I could. This resulted in totally unreadable code with so many bugs it behaved almost randomly. I couldn't fix it, I just rewritten it from the start using exception based approach. Now it works and my job is done, but I want to learn more, so...
The problem is as follows. I have a program which downloads thousands of little files, n
files at a time (simultaneously).
The files are organized in batches, one batch contains about a thousand of files. When whole batch is done, I update some metadata so the app knows which batches are complete, how their structure looks, which files are already downloaded (checksums), and so on.
I can of course click (to start downloading) as many batches as I want, but I have a semaphore which allows only n
concurrent downloads, so nothing bad happens, everything is downloaded in the fastest available way.
Now I have edge cases: the app is suddenly closed during download, a particular batch is canceled or - a network error occurs during download.
In each of the edge cases I need to stop all download tasks immediately, write all calculated checksums and other application state, so the user won't need to redownload the files.
I introduced two CancellationToken
s, one global, which is canceled in application OnExit
override, the second one local, per batch, which is used to cancel particular batch download. Then they are combined using CancellationTokenSource.CreateLinkedTokenSource()
to make the dependent code simpler.
The batch download process is pretty complex and asynchronous, let's say it's in LoadAsync()
method. This method invokes many other methods, creates a list of download tasks, the point is - each dependent asynchronous method takes the combined token and can be canceled in multiple points. Of course HttpClient
also receives cancellation token.
I observed when the cancel event occurs, HttpClient
throws an exception, OpeationCanceledException
type.
So in loops which finally get the exception I have appropriate try
/catch
/finally
blocks to handle them. The key point is the exceptions are propagated through several method calls to be caught by the outermost.
And now it's getting interesting: when I run the code with Visual Studio debugging, the canceling process cause a huge stutter, maximum 1 CPU core load and lasts several seconds. It is because VS stores each exception's call stack and other metadata. Let's say there are thousands of them.
When run without debugging - the canceling is instant, the effect is immediate. All thousands of downloads are canceled in less than 1 GUI frame. The moment I click cancel everything stops exactly the same as I canceled 1 file download.
My question is - what exactly happens when exceptions propagate through the code? Is this an optimal way to cancel multiple complex parallel tasks, or there are way faster alternatives to alter my code flow?
I used token.ThrowIfCancellationRequested()
a lot in the code. Well - it seems like Microsoft uses it anyway in HttpClient
so the resulting code is pretty consistent, neat and readable. Finally
blocks guarantee my semaphore and other disposable resources are properly released leaving very little space for bugs related to cancellation and network errors.
But is it how it should be done, or could it be optimized without sacrificing readability and simplicity?
No code sample, because it would be huge. Simple example would either take me a long time to write (which I don't have right now), or would be too simple to observe what happens when the exceptions propagate through the code.