0

I'm currently working on a concurrent file downloader.

For that reason I want to parametrize the number of concurrent tasks. I don't want to wait for all the tasks to be completed but to keep the same number being runned.

In fact, this thread on star overflow gave me a proper clue, but I'm struggling making it async:

Keep running a specific number of tasks

Here is my code:

public async Task StartAsync()
    {
        var semaphore = new SemaphoreSlim(1, _concurrentTransfers);
        var queueHasMessages = true;

        while (queueHasMessages)
        {
            try {
                await Task.Run(async () =>
                  {
                      await semaphore.WaitAsync();
                      await asyncStuff();
                 });
            }
            finally {
                semaphore.Release();
            };
        }
    }

But the code just get executed one at a time. I think that the await is blocking me for generating the desired amount of tasks, but I don't know how to avoid it while respecting the limit established by the semaphore.

If I add all the tasks to a list and make a whenall, the semaphore throws an exception since it has reached the max count.

Any suggestions?

Roman Pokrovskij
  • 9,449
  • 21
  • 87
  • 142
  • Check [this](https://stackoverflow.com/a/22493662/1768303) – noseratio Aug 01 '18 at 20:18
  • Alternatively, check out the custom task scheduler in [this answer](https://stackoverflow.com/questions/9315937/net-tpl-limited-concurrency-level-task-scheduler-with-task-priority). It provides concurrency limits and you can add additional tasks to the scheduler after execution has already started. – RogerN Aug 01 '18 at 20:27
  • You should keep your Try & Finally inside the task you are creating. Since you are releasing semaphore in finally, it is not consistent with wait. – Daredevil Aug 01 '18 at 20:38

2 Answers2

0

It was brought to my attention that the struck-through solution will drop any exceptions that occur during execution. That's bad.

Here is a solution that will not drop exceptions:


Task.Run is a Factory Method for creating a Task. You can check yourself with the intellisense return value. You can assign the returned Task anywhere you like.

"await" is an operator that will wait until the task it operates on completes. You are able to use any Task with the await operator.

public static async Task RunTasksConcurrently()
{
    IList<Task> tasks = new List<Task>();

    for (int i = 1; i < 4; i++)
    {
        tasks.Add(RunNextTask());
    }

    foreach (var task in tasks) {
        await task; 
    }
}

public static async Task RunNextTask()
{
    while(true) {
        await Task.Delay(500);
    }
}

By adding the values of the Task we create to a list, we can await them later on in execution.


Previous Answer below

Edit: With the clarification I think I understand better.

Instead of running every task at once, you want to start 3 tasks, and as soon as a task is finished, run the next one.

I believe this can happen using the .ContinueWith(Action<Task>) method.

See if this gets closer to your intended solution.

    public void SpawnInitialTasks()
    {
        for (int i = 0; i < 3; i++)
        {
            RunNextTask();
        }
    }

    public void RunNextTask()
    {
        Task.Run(async () => await Task.Delay(500))
            .ContinueWith(t => RunNextTask());  
        // Recurse here to keep running tasks whenever we finish one.
    }

The idea is that we spawn 3 tasks right away, then whenever one finishes we spawn the next. If you need to keep data flowing between the tasks, you can use parameters:

RunNextTask(DataObject object)

etberg
  • 111
  • 1
  • 7
  • Hello! Thank you for answering. My problem is related on how to run N tasks at the same time constantly. In your example I would like to run 1,2,3 and if for example, 3 finishes, then 1,2,4 at the same time. Not making batch of those taks –  Aug 01 '18 at 20:18
  • @AdrianAbreu See if my edit gets closer to what you want. – etberg Aug 01 '18 at 20:36
  • This solution is brilliant! I love it for its simplicity. I'm just struggling now on how to finish the recursive loop but it worked smoothly! –  Aug 01 '18 at 21:10
  • 1
    @AdrianAbreu Why do you find the recursive solution *more* simple than just writing `while(whateverCondition){await DoWork();}`? Sure, you *can* write any loop you ever see as a recursive method, but it's not adding anything. – Servy Aug 01 '18 at 21:57
  • `while(true) { await DoWork(); }` blocks the running task while DoWork is working. He wants to run x tasks concurrently, not in sequence. – etberg Aug 01 '18 at 22:00
  • @etberg It's not different than your `RunNextTask` method, which itself only ever performs *one* task at a time, not multiple. – Servy Aug 01 '18 at 22:14
  • @Servy RunNextTask will return before the Task has completed because Task.Run is not awaited. Three of the tasks that were created by RunNextTask will be working at any given point. https://dotnetfiddle.net/vw0CJO You will see in that fiddle that the output becomes interleaved, which is only possible if the tasks were running concurrently. – etberg Aug 01 '18 at 22:29
  • Use this fiddle instead, I was mean to the CPU on the last one :( https://dotnetfiddle.net/WaDUT7 – etberg Aug 01 '18 at 22:46
  • @etberg The fact that your method is `void` instead of returning the `Task` only ever means that the errors can never be observed and will only ever be dropped on the floor. That's not an advantage, and has *nothing* to do with whether or not the method can run concurrently with other methods. It doesn't matter which implementation you use, you can still return a `Task` or `void` either way. – Servy Aug 02 '18 at 13:23
  • @Servy You're right, that _is_ a big issue. Especially since he is putting download logic in it, which is subject to many external whims. Do you have an alternative solution that answers his question or a modification to mine that will allow the tasks to be observed? – etberg Aug 02 '18 at 14:01
  • @etberg [Sure, it's right here](https://stackoverflow.com/questions/51641406/keep-running-specific-number-of-taks-async/51641557?noredirect=1#comment90250007_51641557). – Servy Aug 02 '18 at 14:05
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/177289/discussion-between-etberg-and-servy). – etberg Aug 02 '18 at 14:58
0

You can do this easily the old-fashioned way without using await by using Parallel.ForEach(), which lets you specify the maximum number of concurrent threads to use.

For example:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    class Program
    {
        public static void Main(string[] args)
        {
            IEnumerable<string> filenames = Enumerable.Range(1, 100).Select(x => x.ToString());

            Parallel.ForEach(
                filenames,
                new ParallelOptions { MaxDegreeOfParallelism = 4},
                download
            );
        }

        static void download(string filepath)
        {
            Console.WriteLine("Downloading " + filepath);
            Thread.Sleep(1000); // Simulate downloading time.
            Console.WriteLine("Downloaded " + filepath);
        }
    }
}

If you run this and observe the output, you'll see that the "files" are being "downloaded" in batchs.

A better simulation is the change download() so that it takes a random amount of time to process each "file", like so:

static Random rng = new Random();

static void download(string filepath)
{
    Console.WriteLine("Downloading " + filepath);
    Thread.Sleep(500 + rng.Next(1000)); // Simulate random downloading time.
    Console.WriteLine("Downloaded " + filepath);
}

Try that and see the difference in the output.


However, if you want a more modern way to do this, you could look into the Dataflow part of the TPL (Task Parallel Library) - this works well with async methods.

This is a lot more complicated to get to grips with, but it's a lot more powerful. You could use an ActionBlock to do it, but describing how to do that is a bit beyond the scope of an answer I could give here.

Have a look at this other answer on StackOverflow; it gives a brief example.

Also note that the TPL is not built in to .Net - you have to get it from NuGet.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276