1

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 CancellationTokens, 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.

Harry
  • 4,524
  • 4
  • 42
  • 81
  • `OperationCanceledException` is a default way to cancel a task in TPL. Why not to switch to [task-oriented approach](https://msdn.microsoft.com/en-us/library/hh873175.aspx) and do not propagate the cancellation exceptions? – VMAtm Nov 03 '16 at 05:28
  • @VMAtm I read, I read, and I read and I can't quite find the difference between my current version of download code and TAP. In my own asynchronous method I can't just access its task object to change its state directly, but I can throw `OperationCanceledException`. It's the most straightforward way. The result is almost identical, task completes with canceled state. `Task.WhenAny()` and `Task.WhenAll()` behave accordingly. BTW the exception passes the control exactly the way it should. It would require a lot of verbose code to emulate this behavior. – Harry Nov 03 '16 at 07:22
  • I thought so too, but yesterday I've investigated [related question](http://stackoverflow.com/a/40368913/213550), and it turns out that: 1. >The preferred way to do this is to use the `ThrowIfCancellationRequested` method. 2. Task behaviour differs from `Task` behaviour when `OperationCanceledException` thrown – VMAtm Nov 03 '16 at 12:45
  • @VMAtm I used `ThrowIfCancellationRequested` wherever possible. It works great, but it's a little unfriendly to debug under VS, since it lags on exceptions a lot. Well, cancellation itself is an exceptional operation so I don't test it often. I test error and cancellation handling without debugging to see its performance under heavy load. – Harry Nov 03 '16 at 16:26
  • You can adjust the exception settings so it wouldn't break on specific type of them, so the debugging will be easier. – VMAtm Nov 03 '16 at 18:40
  • It doesn't break, it just takes more than a minute to log thousands of exceptions. Let's say we have 2k files to download. We downloaded 1000 and got network error. The rest is canceled. It means 1000 tasks get `OperationCanceledException` after initial network exception. Without debugging this case is handled in a fraction of second, with debugging - it takes a minute or two. Of course I could break `await Task.WhenAll` part or use for example `goto` instead of throwing to just complete pending tasks, but it would look horrific and probably wouldn't improve performance at all. – Harry Nov 03 '16 at 21:26

0 Answers0