Hosted service for queuing and running tasks in parallel in ASP.NET Core

I am attempting to construct a background service for an IIS-hosted ASP.NET Core project that basically queues tasks and runs them, and I decided to go with the IHostedService interface in Core 2.1 because of this tutorial. The tutorial does describe the case of running tasks one-by-one and in the order received, very well, and it does suffice for my purposes, but I was wondering whether it would be possible to run tasks in parallel via the same implementation of a BackgroundService, in such a way that one may control the maximum number of tasks running concurrently.

I found this question on SO, but was unable to get the proposed solution to work for the following task creation loop:

for (var i = 0; i < 20; i++) {
var j = i;
await _queue.QueueBackgroundWorkItem(async token =>
{
int s = j % 2 == 0 ? 5 : 1;
Debug.WriteLine("Processed " + j + " in " + s + " seconds");
}
}


where _queue is the IBackgroundTaskQueue defined in the tutorial. Indeed, for the SemaphoreSlim in the solution, but with the initialCount setting set to 4, I still receive the numbers 0, 1, 2, 3, etc. in order.

I then tried to implement my own version of the QueueBackgroundWorkItem and DequeueAsync methods as an implemenation of IBackgroundTaskQueue called NewMultiThreadBackgroundTaskQueue:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace QueueSystem.Queue
{
{

private const int InitialCount = 4;

{
_signal = new SemaphoreSlim(InitialCount);
}

{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}

_workItems.Enqueue(async token =>
{
await _signal.WaitAsync(token);
try
{
await workItem(token);
}
finally
{
_signal.Release();
}
});
}

{
while (!_workItems.IsEmpty && _runItems.Count < InitialCount)
{
_workItems.TryDequeue(out var workItem);
if (workItem != null)
{
}
}

_runItems.Remove(completed);
return async token => await completed;
}
}
}


I use the DequeueAsync method in a QueueHostedService just as in the mentioned tutorial (i.e., nothing has changed):

using System;
using System.Collections.Generic;
using System.Linq;
using QueueSystem.Queue;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace QueueSystem.Services
{
public class QueueHostedService : BackgroundService
{

ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<QueueHostedService>();
}

protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{

while (!cancellationToken.IsCancellationRequested)
{

try
{
await workItem(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error occurred executing {WorkItem}.", nameof(workItem));
}
}

}
}

}


The DequeueAsync method does work as intended in the above background service, but I fear that I'm not using the CancellationToken correctly and my usage of Task.CompletedTask is a bit on the frivolous side.

EDIT:

Following this comment, I have changed the content of the QueueBackgroundWorkItem and DequeueAsync methods:

public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
_workItems.Enqueue(workItem);
}

{

var removed = _runItems.RemoveAll(x => x.IsCompleted);
while (!_workItems.IsEmpty && _runItems.Count < InitialCount)
{
_workItems.TryDequeue(out var workItem);
if (workItem != null)
{

This still works (it dequeues and runs at most InitialCount tasks in parallel), but I'm still curious if there are any obvious improvements that I'm missing.
• _workItems.Enqueue(async token => { await _signal.WaitAsync(token); try { await workItem(token); } finally { _signal.Release(); } });why? why this is this being done... not even sure what its about. surely it should take a task and thats it? the rest is handled by the dequeue, could you explain this Jan 23 at 12:25
• You are correct - SemaphoreSlim is irrelevant. Also, I figured it would be nicer to check if the ConcurrentQueue is empty in the beginning of the DequeueAsync method and return a completed task if it is - then remove all completed tasks at the start of the while loop and return the Task.WhenAny(_runItems) after the while loop. That looks much better. Thanks for the feedback! Feb 4 at 7:45