I am trying to cancel multiple asynchronous web requests (GET) issued via a shared HttpClient using CancellationTokens, in a command-line application, full framework, .net 4.7.1, C# 7.3, VS 2017.
My example runs a couple of parallel tasks, each task is constantly downloading some data over Http, using an async GetAsync() and ReadAsStringAsync() but I get the same result using for example ReadAsByteArrayAsync().
Termination is done by hooking into Console.CancelKeyPress and cancelling my CancellationTokenSource.
Though this seems straight-forward for some reason I cannot get my head around it and come up with a solution that produces a reliable result. Sometimes everything shuts down as expected, i.e. all tasks complete (cancelled) but more often that not shutdown simply seemingly hangs. Running without debugger (with/without DEBUG) terminates the application but not in the way I expected. Fewer tasks means a more likely clean shutdown.
Pausing all threads in debug when in a "hang"-state seems to indicate some threads are stuck in GetAsync() but it is a bit hard for to see exactly what goes on.
In reality it does not matter that much how the application exits but I would like to understand this and be able to produce a clean and controlled shutdown consistently, which to me seems possible using this construct, but I have most likely missed some details.
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace HttpClientAsyncTest
{
class Program
{
static async Task<int> Main()
{
using (var cancellationTokenSource = new CancellationTokenSource())
{
Console.CancelKeyPress += (sender, a) =>
{
Console.WriteLine("Stopped by user");
cancellationTokenSource.Cancel();
};
var task = RunAsync(cancellationTokenSource);
Console.WriteLine("Running - Press CTRL+C to stop");
await task;
}
Console.WriteLine("All done, exiting");
return 0;
}
private static async Task RunAsync(CancellationTokenSource cts)
{
using (var client = new HttpClient())
{
try
{
var tasks = Enumerable.Range(1, 10).Select(id => ProcessBatchAsync(client, id, cts.Token));
await Task.WhenAll(tasks);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation cancelled somehow");
}
}
}
private static async Task ProcessBatchAsync(HttpClient client, int id, CancellationToken cancellationToken)
{
while (true)
await ProcessNextBatchAsync(client, id, cancellationToken);
}
private static async Task ProcessNextBatchAsync(HttpClient client, int id, CancellationToken cancellationToken)
{
using (var response = await client.GetAsync("http://some.payload.to.request", cancellationToken))
{
if (response.StatusCode == HttpStatusCode.NotFound)
return;
var data = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Id: {id} downloaded {data.Length} chars");
}
}
}
}
I expect to see something like this when pressing Ctrl-C:
Running - Press CTRL+C to stop
Id: 6 downloaded 475357 chars
Id: 2 downloaded 475141 chars
Id: 3 downloaded 474927 chars
Id: 5 downloaded 474457 chars
Id: 8 downloaded 474524 chars
Id: 4 downloaded 474643 chars
Id: 7 downloaded 475133 chars
Id: 9 downloaded 475316 chars
Stopped by user
Operation cancelled somehow
All done, exiting
Press any key to continue . . .
But mostly I get something like:
Running - Press CTRL+C to stop
Id: 3 downloaded 474927 chars
Id: 8 downloaded 474524 chars
Id: 5 downloaded 474457 chars
Id: 9 downloaded 475316 chars
Id: 6 downloaded 475357 chars
Id: 7 downloaded 474952 chars
Id: 2 downloaded 475513 chars
Stopped by user
Id: 4 downloaded 475457 chars
Id: 7 downloaded 475133 chars
^CPress any key to continue . . .
Well, rubber ducking myself via StackOverflow worked yet again.
Ctrl+C terminates the application if not canceled via args.Cancel = true, so it has absolulutely zero to do with HttpClient or CancellationToken.
The fix is simply:
Console.CancelKeyPress += (sender, args) =>
{
args.Cancel = true;
Console.WriteLine("Stopped by user");
cancellationTokenSource.Cancel(true);
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With