TPL Queue Processing - c#

I'm currently working on a a project and I have a need to queue some jobs for processing, here's the requirement:
Jobs must be processed one at a time
A queued item must be able to be waited on
So I want something akin to:
Task<result> QueueJob(params here)
{
/// Queue the job and somehow return a waitable task that will wait until the queued job has been executed and return the result.
}
I've tried having a background running task that just pulls items off a queue and processes the job, but the difficulty is getting from a background task to the method.
If need be I could go the route of just requesting a completion callback in the QueueJob method, but it'd be great if I could get a transparent Task back that allows you to wait on the job to be processed (even if there are jobs before it in the queue).

You might find TaskCompletionSource<T> useful, it can be used to create a Task that completes exactly when you want it to. If you combine it with BlockingCollection<T>, you will get your queue:
class JobProcessor<TInput, TOutput> : IDisposable
{
private readonly Func<TInput, TOutput> m_transform;
// or a custom type instead of Tuple
private readonly
BlockingCollection<Tuple<TInput, TaskCompletionSource<TOutput>>>
m_queue =
new BlockingCollection<Tuple<TInput, TaskCompletionSource<TOutput>>>();
public JobProcessor(Func<TInput, TOutput> transform)
{
m_transform = transform;
Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
}
private void ProcessQueue()
{
Tuple<TInput, TaskCompletionSource<TOutput>> tuple;
while (m_queue.TryTake(out tuple, Timeout.Infinite))
{
var input = tuple.Item1;
var tcs = tuple.Item2;
try
{
tcs.SetResult(m_transform(input));
}
catch (Exception ex)
{
tcs.SetException(ex);
}
}
}
public Task<TOutput> QueueJob(TInput input)
{
var tcs = new TaskCompletionSource<TOutput>();
m_queue.Add(Tuple.Create(input, tcs));
return tcs.Task;
}
public void Dispose()
{
m_queue.CompleteAdding();
}
}

I would go for something like this:
class TaskProcessor<TResult>
{
// TODO: Error handling!
readonly BlockingCollection<Task<TResult>> blockingCollection = new BlockingCollection<Task<TResult>>(new ConcurrentQueue<Task<TResult>>());
public Task<TResult> AddTask(Func<TResult> work)
{
var task = new Task<TResult>(work);
blockingCollection.Add(task);
return task; // give the task back to the caller so they can wait on it
}
public void CompleteAddingTasks()
{
blockingCollection.CompleteAdding();
}
public TaskProcessor()
{
ProcessQueue();
}
void ProcessQueue()
{
Task<TResult> task;
while (blockingCollection.TryTake(out task))
{
task.Start();
task.Wait(); // ensure this task finishes before we start a new one...
}
}
}
Depending on the type of app that is using it, you could switch out the BlockingCollection/ConcurrentQueue for something simpler (eg just a plain queue). You can also adjust the signature of the "AddTask" method depending on what sort of methods/parameters you will be queueing up...

Func<T> takes no parameters and returns a value of type T. The jobs are run one by one and you can wait on the returned task to get the result.
public class TaskQueue
{
private Queue<Task> InnerTaskQueue;
private bool IsJobRunning;
public void Start()
{
Task.Factory.StartNew(() =>
{
while (true)
{
if (InnerTaskQueue.Count > 0 && !IsJobRunning)
{
var task = InnerTaskQueue.Dequeue()
task.Start();
IsJobRunning = true;
task.ContinueWith(t => IsJobRunning = false);
}
else
{
Thread.Sleep(1000);
}
}
}
}
public Task<T> QueueJob(Func<T> job)
{
var task = new Task<T>(() => job());
InnerTaskQueue.Enqueue(task);
return task;
}
}

Related

Awaiting manually created task is freezing ASP.NET app

I've made a queue, which contains tasks to do. After creating some tasks manually with new Task() in Returns method, my whole application hangs - await current;. The body of the task is not even triggered.
ConfigureAwait(false) didn't help.
The first task in the queue, which is not created by me, but other framework is executing successfully and returning a value. Mine - doesn't. I've tried add Task.CompletedTask and then it has worked. I don't understand why I can't even reach the body of the task containing _output assignment.
IDE debugger code screenshot
---UPDATE---
The code works when I use code below. With await it doesn't. Any ideas?
current.Start();
current.Wait();
Original code
private readonly Queue<Task> _pipe;
public IPipeBuilder<TOutput> Returns(Func<IEnumerable<IExecutionResult>, TOutput> outputBuilder)
{
_pipe.Enqueue(new Task(() => // this task causes a problem and breakpoint isn't hit inside
{
_output = outputBuilder(_results);
}));
return this;
}
public async Task<TOutput> Execute()
{
Task current;
while (_pipe.TryDequeue(out current))
{
if (current.IsCommandExecution())
{
IExecutionResult result = await (Task<IExecutionResult>)current; // this awaits successfully
_results.Add(result);
}
else
{
await current; // hangs here
}
}
return await Task.FromResult(_output);
}
Usage
[HttpGet("eventflow/pipe/issue/add/{title}")]
public async Task<IActionResult> PipeAction(string title)
=> Ok(
await Pipe<IExecutionResult>()
.Validate(title)
.Handle<AddIssueCommand>(IssueId.New, title)
.Returns(results => results.First())
.Execute());
You should never use the Task constructor. This goes double on ASP.NET, since constructed tasks are always Delegate Tasks, which interfere with the ASP.NET usage of the thread pool. The actual reason that the await hangs is because manually-created tasks need to be started.
If you have synchronous work that you need to wrap into a Task to work alongside asynchronous tasks, then you should use Task.CompletedTask and Task.FromException:
private static Task SynchronousWork(Func<IEnumerable<IExecutionResult>, TOutput> outputBuilder)
{
try { _output = outputBuilder(_results); return Task.CompletedTask; }
catch (Exception ex) { return Task.FromException(ex); }
}
public IPipeBuilder<TOutput> Returns(Func<IEnumerable<IExecutionResult>, TOutput> outputBuilder)
{
_pipe.Enqueue(SynchronousWork(outputBuilder));
return this;
}
However, note that this executes outputBuilder immediately, which may not be desirable due to its side effects on _results and _output. If you want a delayed execution queue, then the type in the queue needs to be changed from Task to Func<Task>. Then you can add to it as such:
public IPipeBuilder<TOutput> Returns(Func<IEnumerable<IExecutionResult>, TOutput> outputBuilder)
{
_pipe.Enqueue(() =>
{
try { _output = outputBuilder(_results); return Task.CompletedTask; }
catch (Exception ex) { return Task.FromException(ex); }
});
return this;
}
and you would consume it by calling each delegate one at a time and inspecting the task it returns:
public async Task<TOutput> Execute()
{
while (_pipe.TryDequeue(out var currentFunc))
{
var currentTask = currentFunc();
if (currentTask.IsCommandExecution())
{
IExecutionResult result = await (Task<IExecutionResult>)currentTask;
_results.Add(result);
}
else
{
await currentTask;
}
}
return _output;
}
Okay, thank you. I've ended up with such class and Queue<Func<Task>> like you said.
public sealed class SyncTaskWrapper
{
private Func<Task> _action;
public SyncTaskWrapper(Action action)
=> _action = CreateFunc(action);
private static Func<Task> CreateFunc(Action action)
=> () =>
{
try
{
action();
return Task.CompletedTask;
}
catch (Exception exception)
{
return Task.FromException(exception);
}
};
public static implicit operator Func<Task>(SyncTaskWrapper #this)
=> #this._action;
}
with usage
_pipe.Enqueue(new SyncTaskWrapper(() =>
_output = outputBuilder(_results)));

How to cancel Task without exception?

I need to execute a kind of LongRunning task after a delay.
Each Task can be cancelled. I prefer TPL with cancellationToken.
Since my task is long running and before starting a task it has to be placed in dictionary I have to use new Task(). But I've faced different behavior - when task is created using new Task() after Cancel() it throws TaskCanceledException whereas a task created with Task.Run doesn't throw an exception.
Generally I need to recognize the difference and not get TaskCanceledException.
It's my code:
internal sealed class Worker : IDisposable
{
private readonly IDictionary<Guid, (Task task, CancellationTokenSource cts)> _tasks =
new Dictionary<Guid, (Task task, CancellationTokenSource cts)>();
public void ExecuteAfter(Action action, TimeSpan waitBeforeExecute, out Guid cancellationId)
{
var cts = new CancellationTokenSource();
var task = new Task(async () =>
{
await Task.Delay(waitBeforeExecute, cts.Token);
action();
}, cts.Token, TaskCreationOptions.LongRunning);
cancellationId = Guid.NewGuid();
_tasks.Add(cancellationId, (task, cts));
task.Start(TaskScheduler.Default);
}
public void ExecuteAfter2(Action action, TimeSpan waitBeforeExecute, out Guid cancellationId)
{
var cts = new CancellationTokenSource();
cancellationId = Guid.NewGuid();
_tasks.Add(cancellationId, (Task.Run(async () =>
{
await Task.Delay(waitBeforeExecute, cts.Token);
action();
}, cts.Token), cts));
}
public void Abort(Guid cancellationId)
{
if (_tasks.TryGetValue(cancellationId, out var value))
{
value.cts.Cancel();
//value.task.Wait();
_tasks.Remove(cancellationId);
Dispose(value.cts);
Dispose(value.task);
}
}
public void Dispose()
{
if (_tasks.Count > 0)
{
foreach (var t in _tasks)
{
Dispose(t.Value.cts);
Dispose(t.Value.task);
}
_tasks.Clear();
}
}
private static void Dispose(IDisposable obj)
{
if (obj == null)
{
return;
}
try
{
obj.Dispose();
}
catch (Exception ex)
{
//Log.Exception(ex);
}
}
}
internal class Program
{
private static void Main(string[] args)
{
Action act = () => Console.WriteLine("......");
Console.WriteLine("Started");
using (var w = new Worker())
{
w.ExecuteAfter(act, TimeSpan.FromMilliseconds(10000), out var id);
//w.ExecuteAfter2(act, TimeSpan.FromMilliseconds(10000), out var id);
Thread.Sleep(3000);
w.Abort(id);
}
Console.WriteLine("Enter to exit");
Console.ReadKey();
}
}
UPD:
This approach also works without exception
public void ExecuteAfter3(Action action, TimeSpan waitBeforeExecute, out Guid cancellationId)
{
var cts = new CancellationTokenSource();
cancellationId = Guid.NewGuid();
_tasks.Add(cancellationId, (Task.Factory.StartNew(async () =>
{
await Task.Delay(waitBeforeExecute, cts.Token);
action();
}, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default), cts)); ;
}
The reason of the inconsistent behavior is fundamentally incorrect usage of an async delegate in the first case. The Task constructors just don't receive Func<Task> and your asynchronous delegate is always interpreted as async void not async Task in case of using with constructor. If an exception is raised in an async Task method it's caught and placed into Task object which isn't true for an async void method, in that case exception just bubbles up out of the method to a synchronization context and goes under category of unhandled exceptions (you can familiarize with details in this Stephen Cleary article). So what happens in case of using constructor: a task which is supposed to initiate asynchronous flow is created and started. Once it reaches point when Task.Delay(...) returns a promise, the task completes and it has no more relationship to anything which happens in Task.Delay continuation (you can easily check in debugger by setting a breakpoint to value.cts.Cancel() that the task object in the _tasks dictionary has status RanToCompletetion while however the task delegate essentially is still running). When a cancellation is requested the exception is raised inside the Task.Delay method and without existence of any promise object is being promoted to app domain.
In case of Task.Run the situation is different because there are overloads of this method which are able to accept Func<Task> or Func<Task<T>> and unwrap the tasks internally in order to return underlying promise instead of wrapped task which ensures proper task object inside the _tasks dictionary and proper error handling.
The third scenario despite the fact that it doesn't throw an exception it is partially correct. Unlike Task.Run, Task.Factory.StartNew doesn't unwrap underlying task to return promise, so task stored in the _tasks is just wrapper task, like in the case with constructor (again you can check its state with debugger). It however is able to understand Func<Task> parameters, so asynchronous delegate has async Task signature which allows at least to handle and store exception in the underlying task. In order to get this underlying task with Task.Factory.StartNew you need to unwrap the task by yourself with Unwrap() extension method.
The Task.Factory.StartNew isn't considered as a beast practice of creating tasks because of certain dangers related to its application (see there). It however can be used with some caveats if you need to apply specific options like LongRunning which cannot be directly applied with Task.Run.
I don't know why I got down votes here but it's inspired me to update my answer.
UPDATED
My full approach:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp4
{
internal class Program
{
private static void Main(string[] args)
{
using (var delayedWorker = new DelayedWorker())
{
delayedWorker.ProcessWithDelay(() => { Console.WriteLine("100"); }, TimeSpan.FromSeconds(5), out var cancellationId_1);
delayedWorker.ProcessWithDelay(() => { Console.WriteLine("200"); }, TimeSpan.FromSeconds(10), out var cancellationId_2);
delayedWorker.ProcessWithDelay(() => { Console.WriteLine("300"); }, TimeSpan.FromSeconds(15), out var cancellationId_3);
Cancel_3(delayedWorker, cancellationId_3);
Console.ReadKey();
}
}
private static void Cancel_3(DelayedWorker delayedWorker, Guid cancellationId_3)
{
Task.Run(() => { delayedWorker.Abort(cancellationId_3); }).Wait();
}
internal sealed class DelayedWorker : IDisposable
{
private readonly object _locker = new object();
private readonly object _disposeLocker = new object();
private readonly IDictionary<Guid, (Task task, CancellationTokenSource cts)> _tasks = new Dictionary<Guid, (Task task, CancellationTokenSource cts)>();
private bool _disposing;
public void ProcessWithDelay(Action action, TimeSpan waitBeforeExecute, out Guid cancellationId)
{
Console.WriteLine("Creating delayed action...");
CancellationTokenSource tempCts = null;
CancellationTokenSource cts = null;
try
{
var id = cancellationId = Guid.NewGuid();
tempCts = new CancellationTokenSource();
cts = tempCts;
var task = new Task(() => { Process(action, waitBeforeExecute, cts); }, TaskCreationOptions.LongRunning);
_tasks.Add(cancellationId, (task, cts));
tempCts = null;
task.ContinueWith(t =>
{
lock (_disposeLocker)
{
if (!_disposing)
{
TryRemove(id);
}
}
}, TaskContinuationOptions.ExecuteSynchronously);
Console.WriteLine($"Created(cancellationId: {cancellationId})");
task.Start(TaskScheduler.Default);
}
finally
{
if (tempCts != null)
{
tempCts.Dispose();
}
}
}
private void Process(Action action, TimeSpan waitBeforeExecute, CancellationTokenSource cts)
{
Console.WriteLine("Starting delayed action...");
cts.Token.WaitHandle.WaitOne(waitBeforeExecute);
if (cts.Token.IsCancellationRequested)
{
return;
}
lock (_locker)
{
Console.WriteLine("Performing action...");
action();
}
}
public bool Abort(Guid cancellationId)
{
Console.WriteLine($"Aborting(cancellationId: {cancellationId})...");
lock (_locker)
{
if (_tasks.TryGetValue(cancellationId, out var value))
{
if (value.task.IsCompleted)
{
Console.WriteLine("too late");
return false;
}
value.cts.Cancel();
value.task.Wait();
Console.WriteLine("Aborted");
return true;
}
Console.WriteLine("Either too late or wrong cancellation id");
return true;
}
}
private void TryRemove(Guid id)
{
if (_tasks.TryGetValue(id, out var value))
{
Remove(id, value.task, value.cts);
}
}
private void Remove(Guid id, Task task, CancellationTokenSource cts)
{
_tasks.Remove(id);
Dispose(cts);
Dispose(task);
}
public void Dispose()
{
lock (_disposeLocker)
{
_disposing = true;
}
if (_tasks.Count > 0)
{
foreach (var t in _tasks)
{
t.Value.cts.Cancel();
t.Value.task.Wait();
Dispose(t.Value.cts);
Dispose(t.Value.task);
}
_tasks.Clear();
}
}
private static void Dispose(IDisposable obj)
{
if (obj == null)
{
return;
}
try
{
obj.Dispose();
}
catch (Exception ex)
{
//log ex
}
}
}
}
}

Queuing tasks in asp.net core

E.g. of functionality There is 20 users and they clicked send button almost in one time, so methods stacking in queue and first user message is sent and response received, after second third and so on. Users wont chat with other people but with device which response is pretty fast
So I am trying to queue Task which sends Message.
I found code samples that uses Task queuing as shown in Example 1 and Example 2.
Example 1
public class SerialQueue
{
readonly object _locker = new object();
WeakReference<Task> _lastTask;
public Task Enqueue(Action action)
{
return Enqueue<object>(() => {
action();
return null;
});
}
public Task<T> Enqueue<T>(Func<T> function)
{
lock (_locker)
{
Task lastTask = null;
Task<T> resultTask = null;
if (_lastTask != null && _lastTask.TryGetTarget(out lastTask))
{
resultTask = lastTask.ContinueWith(_ => function());
}
else
{
resultTask = Task.Run(function);
}
_lastTask = new WeakReference<Task>(resultTask);
return resultTask;
}
}
}
Example 2
public class TaskQueue
{
private readonly SemaphoreSlim _semaphoreSlim;
public TaskQueue()
{
_semaphoreSlim = new SemaphoreSlim(1);
}
public async Task<T> Enqueue<T>(Func<Task<T>> taskGenerator)
{
await _semaphoreSlim.WaitAsync();
try
{
return await taskGenerator();
}
finally
{
_semaphoreSlim.Release();
}
}
public async Task Enqueue(Func<Task> taskGenerator)
{
await _semaphoreSlim.WaitAsync();
try
{
await taskGenerator();
}
finally
{
_semaphoreSlim.Release();
}
}
}
Problem is that when I'm passing task which I want to queue (Example 3) each time I pressing button, tasks still are executed at the same time and interrupting each other.
Example 3
[HttpPost(Name = "add-message")]
public async Task<IActionResult> PostMessage([FromBody] MessengerViewModel messengerViewModel)
{
TaskQueue taskQueue = new TaskQueue();
SerialQueue serialQueue = new SerialQueue();
await taskQueue.Enqueue(() => SendMessage(messengerViewModel.PhoneNr, messengerViewModel.MessageBody,
messengerViewModel.ContactId, messengerViewModel.State));
//I'm not running tasks at same time, using one or other at time
await serialQueue.Enqueue(() => SendMessage(messengerViewModel.PhoneNr, messengerViewModel.MessageBody,
messengerViewModel.ContactId, messengerViewModel.State));
return Ok();
}
How could I solve problem and stack task to queue by each click?
Your problem is that you create a new TaskQueueand SerialQueue everytime. Thus each time a user clicks/invokes PostMessage a new queue is created, and the task is the first task in the queue and executed directly.
You should use a static/singleton queue so each click/invoke works on the same queue object.
But that would deliver problems when you scale your webapp across multiple servers. To that end you should use things like (for example) Azure Queue Storage in combination with Azure Functions.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<TaskQueue>();
services.AddSingleton<SerialQueue>();
// the rest
}
SomeController.cs
[HttpPost(Name = "add-message")]
public async Task<IActionResult> PostMessage(
[FromBody] MessengerViewModel messengerViewModel,
[FromServices] TaskQueue taskQueue,
[FromServices] SerialQueue serialQueue)
{
await taskQueue.Enqueue(
() => SendMessage(
messengerViewModel.PhoneNr,
messengerViewModel.MessageBody,
messengerViewModel.ContactId,
messengerViewModel.State));
//I'm not running tasks at same time, using one or other at time
await serialQueue.Enqueue(
() => SendMessage(
messengerViewModel.PhoneNr,
messengerViewModel.MessageBody,
messengerViewModel.ContactId,
messengerViewModel.State));
return Ok();
}

Async wait for multiple threads to finish

I have a code block which is eventually accessed by multiple threads. I search for an up to date async mechanism to continue executing when all threads have passed.
Currently I do the following with a CountDownEvent which works just fine (without async support).
public class Watcher
{
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private readonly CountdownEvent _isUpdating = new CountdownEvent(1);
private readonly IActivity _activity;
public Watcher([NotNull] IActivity activity)
{
_activity = activity ?? throw new ArgumentNullException(nameof(activity));
_activity.Received += OnReceived;
}
private void OnReceived(IReadOnlyCollection<Summary> summaries)
{
_isUpdating.AddCount();
try
{
// Threads processing
}
finally
{
_isUpdating.Signal();
}
}
private void Disable()
{
_activity.Received -= OnReceived;
_isUpdating.Signal();
/* await */ _isUpdating.Wait();
}
}
Do I need to use any of those AsyncCountdownEvent implementations or is there any other built-in mechanism? I already thought about using a BufferBlock because it has async functionality but I think it's a bit overkill.
Additional to the comments:
IActivity is a WebService call (but shouldn't effect the implementation on top or vice versa)
public async Task Start(bool alwayRetry = true, CancellationToken cancellationToken = new CancellationToken())
{
var milliseconds = ReloadSeconds * 1000;
do
{
try
{
var summaries = await PublicAPI.GetSummariesAsync(cancellationToken).ConfigureAwait(false);
OnSummariesReceived(summaries);
}
catch (Exception ex)
{
Log.Error(ex.Message);
OnErrorOccurred(ex);
}
await Task.Delay(milliseconds, cancellationToken).ConfigureAwait(false);
// ReSharper disable once LoopVariableIsNeverChangedInsideLoop
} while (alwayRetry);
}
It's not clear the IActivity signatures; but you can wait for a range of tasks to be completed:
class MultiAsyncTest {
Task SomeAsync1() { return Task.Delay(1000); }
Task SomeAsync2() { return Task.Delay(2000);}
Task EntryPointAsync() {
var tasks = new List<Task>();
tasks.Add(SomeAsync1());
tasks.Add(SomeAsync2());
return Task.WhenAll(tasks);
}
}
What's IActivity's signature? Does it support Task? Or you are using Thread? More explanation would help to a more specified answer.

Deadlock when combining app domain remoting and tasks

My app needs to load plugins into separate app domains and then execute some code inside of them asynchronously. I've written some code to wrap Task in marshallable types:
static class RemoteTask
{
public static async Task<T> ClientComplete<T>(RemoteTask<T> remoteTask,
CancellationToken cancellationToken)
{
T result;
using (cancellationToken.Register(remoteTask.Cancel))
{
RemoteTaskCompletionSource<T> tcs = new RemoteTaskCompletionSource<T>();
remoteTask.Complete(tcs);
result = await tcs.Task;
}
await Task.Yield(); // HACK!!
return result;
}
public static RemoteTask<T> ServerStart<T>(Func<CancellationToken, Task<T>> func)
{
return new RemoteTask<T>(func);
}
}
class RemoteTask<T> : MarshalByRefObject
{
readonly CancellationTokenSource cts = new CancellationTokenSource();
readonly Task<T> task;
internal RemoteTask(Func<CancellationToken, Task<T>> starter)
{
this.task = starter(cts.Token);
}
internal void Complete(RemoteTaskCompletionSource<T> tcs)
{
task.ContinueWith(t =>
{
if (t.IsFaulted)
{
tcs.TrySetException(t.Exception);
}
else if (t.IsCanceled)
{
tcs.TrySetCancelled();
}
else
{
tcs.TrySetResult(t.Result);
}
}, TaskContinuationOptions.ExecuteSynchronously);
}
internal void Cancel()
{
cts.Cancel();
}
}
class RemoteTaskCompletionSource<T> : MarshalByRefObject
{
readonly TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
public bool TrySetResult(T result) { return tcs.TrySetResult(result); }
public bool TrySetCancelled() { return tcs.TrySetCanceled(); }
public bool TrySetException(Exception ex) { return tcs.TrySetException(ex); }
public Task<T> Task
{
get
{
return tcs.Task;
}
}
}
It's used like:
sealed class ControllerAppDomain
{
PluginAppDomain plugin;
public Task<int> SomethingAsync()
{
return RemoteTask.ClientComplete(plugin.SomethingAsync(), CancellationToken.None);
}
}
sealed class PluginAppDomain : MarshalByRefObject
{
public RemoteTask<int> SomethingAsync()
{
return RemoteTask.ServerStart(async cts =>
{
cts.ThrowIfCancellationRequested();
return 1;
});
}
}
But I've run into a snag. If you look in ClientComplete, there's a Task.Yield() I've inserted. If I comment this line, ClientComplete will never return. Any ideas?
My best guess is that you are facing these issues because of the async method that contains await and this is managed via the ThreadPool which can allocate some recycled Thread.
Reference
Best practice to call ConfigureAwait for all server-side code
Actually, just doing an await can do that(put you on a different thread). Once your async method hits
an await, the method is blocked but the thread returns to the thread
pool. When the method is ready to continue, any thread is snatched
from the thread pool and used to resume the method.
Try to streamline the code, generate threads for baseline cases and
performance is last.

Categories