SemaphoreSlim limit tasks
It seems you simply need process a fixed set of work items in parallel with a fixed degree of parallelism and in an async compatible way. Stephen Toub has written a very elegant way to do that in just a couple lines of code.

public static Task ForEachAsync&lt;T&gt;(this IEnumerable&lt;T&gt; source, int dop, Func&lt;T, Task&gt; body) 
{ 
    return Task.WhenAll( 
        from partition in Partitioner.Create(source).GetPartitions(dop) 
        select Task.Run(async delegate { 
            using (partition) 
                while (partition.MoveNext()) 
                    await body(partition.Current); 
        })); 
}


Your new code would be:

await ForEachAsync(GetWorkItems(), dop: 16, body: async item =&gt; {
 await ProcessItem(item); //TODO
});


There is no need to explicitly maintain a queue. But if you want to, you can du that by feeding ConcurrentQueue.GetConsumingEnumerable() into that ForEachAsync helper.