-1

When I schedule a lot of tasks, and some tasks go into delay mode, I would expect others to start running, but the following code demonstrates that this does not happen.

I limited the MaxDegreeOfParallelism to 3, you can see that a new group of 3 tasks is activated only after the first 3 are finished, although it was possible to start them immediately when the first three entered the delay.

using System;
using System.Diagnostics;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var s = new Stopwatch();
            s.Start();
            Parallel.For(1, 12, new ParallelOptions() { MaxDegreeOfParallelism = 3 }, (i) =>
            {
                Debug.WriteLine($"Task: {i}, Elapsed: {s.Elapsed}");
                Task.WaitAll(Task.Delay(5000));
            });
        }
    }
}

Output:

Task: 4, Elapsed: 00:00:00.0811475
Task: 1, Elapsed: 00:00:00.0811470
Task: 7, Elapsed: 00:00:00.0811479
Task: 2, Elapsed: 00:00:05.0972314
Task: 8, Elapsed: 00:00:05.1036003
Task: 5, Elapsed: 00:00:05.1036003
Task: 9, Elapsed: 00:00:10.1058859
Task: 3, Elapsed: 00:00:10.1101862
Task: 6, Elapsed: 00:00:10.1356772
Task: 10, Elapsed: 00:00:15.1183184
Task: 11, Elapsed: 00:00:20.1289868

The same output is obtained in both TargetFramework net48 and TargetFramework netcoreapp3.1.

How is it possible to make that when some of the tasks are in sleep or delay mode then others will come into action?

As far as I know, Windows behaves with threads, that when one sleeps, Windows switches to run another thread. Similarly, I also need tasks.

Maybe I should work with something else and not tasks? Maybe with threads? But how exactly?

To summarize what I need:

  1. Run many actions at the same time and according to the turn.
  2. Possibility to limit the operations to X operations at the same time.
  3. In the event that one operation enters standby or sleep mode, another can start or continue work.
  4. When an action finishes the sleep period it returns to work according to the turn as a new action that never started.
codeDom
  • 1,623
  • 18
  • 54
  • 2
    It's not, that would conflict with the configured degree of parallelism. Imagine a thread that goes to sleep for 5 seconds and a new one comes into action that does work taking more than 5 seconds. When the first thread resumes, how is the second one supposed to stop? It isn't, of course, so you'll exceed the limit of at most 3 threads in action. If you want this sort of thing more intricate synchronization is needed (like threads grabbing a semaphore, or indeed tasks -- what you're doing with synchronous waiting now is not what tasks are intended for). – Jeroen Mostert Apr 10 '23 at 14:32
  • @JeroenMostert The task that went to sleep will be activated again in turn, like any other task that has not yet started. So it may come back after more than 5 seconds set to sleep. But the second task won't stop after 5 seconds. – codeDom Apr 10 '23 at 14:35
  • That would work, but that would require cooperative synchronization or a custom scheduler, which `Parallel.For` and threads in general do not offer out of the box. For tasks there are things like `QueuedTaskScheduler`. – Jeroen Mostert Apr 10 '23 at 14:40
  • 1
    netcoreapp3.1 – codeDom Apr 10 '23 at 17:53

2 Answers2

2

I limited the MaxDegreeOfParallelism to 3, you can see that a new group of 3 tasks is activated only after the first 3 are finished, although it was possible to start them immediately when the first three entered the delay.

No, it is not possible from the Parallel.For standpoint of view. MaxDegreeOfParallelism means that it will have at most 3 actions executing in parallel no matter what they are doing.

Also note Parallel.For is not Task-aware (so no fancy async-await stuff handling) and Task.WaitAll is effectively a blocking call which for example does not allow returning of the executing thread into the thread pool.

As far as I know, Windows behaves with threads, that when one sleeps, Windows switches to run another thread. Similarly, I also need tasks.

That is handled by the ThreadPool. Essentially this was one the goals of async-await introduction - to easily handle IO-bound operations allowing to reuse the threads to do meaningful work while doing the IO waiting. You can just start all of your tasks and let the thread pool handle the rest (though there are some potential issues - you can't create a custom thread pool in .NET and you can end up with thread pool starvation).

Note that you can create a custom task scheduler - see the LimitedConcurrencyLevelTaskScheduler from TaskScheduler docs.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
2

In case you are able to find corresponding asynchronous APIs for all your standby/wait/sleep operations, you could achieve the desirable behavior by using a ConcurrentExclusiveSchedulerPair. For example the blocking Task.WaitAll(Task.Delay(5000)) can be replaced by the asynchronous await Task.Delay(5000). Here is a demo:

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;

class Program
{
    static void Main(string[] args)
    {
        TaskScheduler scheduler = new ConcurrentExclusiveSchedulerPair(
            TaskScheduler.Default, maxConcurrencyLevel: 3).ConcurrentScheduler;

        TaskFactory factory = new TaskFactory(scheduler);

        Stopwatch stopwatch = Stopwatch.StartNew();

        Task[] tasks = Enumerable.Range(1, 12).Select(i => factory.StartNew(async () =>
        {
            Thread.Sleep(5000); // This counts towards the maxConcurrencyLevel
            Console.WriteLine($"Task: {i}, Elapsed: {stopwatch.Elapsed}");
            await Task.Delay(5000); // This doesn't count towards the maxConcurrencyLevel
        }).Unwrap()).ToArray();

        Task.WaitAll(tasks);
    }
}

Online demo.

This works because TaskSchedulers are not aware of the asynchronous dimension. They only know how to execute code that uses threads, and asynchronous operations typically fly above threads.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104