Reputation: 61
Scenario is to queue tons of tasks of various kinds, throttle them for parallel processing, and be able to cancel them. My problem is that cancelling them actually takes longer than the tasks themselves, due to all tasks already hanging in semaphore.
Obviously, the .Run throws all tasks into the .WaitAsync, and are therefor having status WaitingForActiviation. So the token given to the task itself is in fact pointless: All 1000 tasks are already running.
Giving the token to WaitAsync seems to freeze the application on cancel.
static SemaphoreSlim batcher = new SemaphoreSlim(5);
static void Main(string[] args)
{
var tokenStore = new CancellationTokenSource();
var tasks = Enumerable.Range(1, 1000).Select(i =>
DoableWork(tokenStore.Token)
).ToList();
do {
if (Console.KeyAvailable && Console.ReadKey(true).Key == ConsoleKey.Escape) {
Console.WriteLine("Cancelling tasks");
tokenStore.Cancel();
}
Thread.Sleep(1000);
Console.WriteLine($"Tasks: {string.Join(", ", tasks.GroupBy(t => t.Status).Select(x => $"{x.Count()} {x.Key}"))}");
} while (tasks.Any(t => t.Status < TaskStatus.RanToCompletion));
}
private static Task DoableWork(CancellationToken token)
{
return Task.Run(async () => {
try {
await batcher.WaitAsync();
token.ThrowIfCancellationRequested();
await Task.Delay(200, token); // Do stuff
} catch (OperationCanceledException) {
throw;
} catch (Exception) {
// Logging
throw;
} finally {
batcher.Release();
}
}, token);
}
Results:
Tasks: 25 RanToCompletion, 975 WaitingForActivation
Tasks: 50 RanToCompletion, 950 WaitingForActivation
Tasks: 75 RanToCompletion, 925 WaitingForActivation
Tasks: 100 RanToCompletion, 900 WaitingForActivation
Tasks: 120 RanToCompletion, 880 WaitingForActivation
Tasks: 145 RanToCompletion, 855 WaitingForActivation
Cancelling tasks
Tasks: 145 RanToCompletion, 16 Canceled, 839 WaitingForActivation
Tasks: 145 RanToCompletion, 33 Canceled, 822 WaitingForActivation
Tasks: 145 RanToCompletion, 51 Canceled, 804 WaitingForActivation
Tasks: 145 RanToCompletion, 66 Canceled, 789 WaitingForActivation
Tasks: 145 RanToCompletion, 81 Canceled, 774 WaitingForActivation
Tasks: 145 RanToCompletion, 101 Canceled, 754 WaitingForActivation
As you can see, there's less tasks cancelled per second, than handled. At this rate, you'ld have the wait a full minute to get the 1000 pending tasks cancelled.
Making an alternative with new Task(), and calling Task.Start() will result in breaking the cancellation mechanic, instead throwing unhandled exceptions from each of the running tasks.
tasks.Where(t => t.Status < TaskStatus.Running)
.Take(5 - tasks.Count(t => t.Status == TaskStatus.Running))
.ToList()
.ForEach(t => t.Start());
private static Task DoableWork(CancellationToken token)
{
return new Task(async () => {
try {
await Task.Delay(200, token); // Do stuff
} catch (OperationCanceledException) {
throw;
} catch (Exception) {
// Logging
throw;
}
}, token);
}
Upvotes: 1
Views: 533
Reputation: 61
Found the issue. It's VS 2019 (or one of its settings) that's slowing down the exception handling significantly in debug mode. When I run the .exe directly, cancellation is instant. Thanks to @TheodorZoulias for showing a failure to reproduce the issue.
Upvotes: 1