Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does Task.Delay start a new thread?

The following code should (at least in my opinion) create 100 Tasks, which are all waiting in parallel (that's the point about concurrency, right :D ?) and finish almost at the same time. I guess for every Task.Delay a Timerobject is created internally.

public static async Task MainAsync() {

    var tasks = new List<Task>();
    for (var i = 0; i < 100; i++) {
        Func<Task> func = async () => {
            await Task.Delay(1000);
            Console.WriteLine("Instant");
        };
        tasks.Add(func());
    }
    await Task.WhenAll(tasks);
}

public static void Main(string[] args) {
    MainAsync().Wait();
}

But! When I run this on Mono I get very strange behavior:

  • The Tasks do not finish at the same time, there are huge delays (probably about 500-600ms)
  • In the console mono shows a lot of created threads:

Loaded assembly: /Users/xxxxx/Programming/xxxxx/xxxxxxxxxx/bin/Release/xxxxx.exe

Thread started: #2

Thread started: #3

Thread started: #4

Thread started: #5

Thread started: #6

Thread started: #7

Thread finished: #3 <-- Obviously the delay of 1000ms finished ?

Thread finished: #2 <-- Obviously the delay of 1000ms finished ?

Thread started: #8

Thread started: #9

Thread started: #10

Thread started: #11

Thread started: #12

Thread started: #13

... you get it.

Is this actually a bug ? Or do I use the library wrong ?

[EDIT] I tested a custom sleep method using Timer:

    public static async Task MainAsync() {
        Console.WriteLine("Started");
        var tasks = new List<Task>();
        for (var i = 0; i < 100; i++) {
            Func<Task> func = async () => {
                await SleepFast(1000);
                Console.WriteLine("Instant");
            };
            tasks.Add(func());
        }
        await Task.WhenAll(tasks);
        Console.WriteLine("Ready");
    }

    public static Task SleepFast(int amount) {
        var source = new TaskCompletionSource<object>();
        new Timer(state => {
            var oldSrc = (TaskCompletionSource<object>)state;
            oldSrc.SetResult(null);
        }, source, amount, 0);
        return source.Task;
    }

This time, all tasks completed instantaneously. So, I think it's a really bad implementation or a bug.

[Edit2] Just FYI: I've tested the original code (using Task.Delay) on .NET using Windows 8.1 now and it ran as expected (1000 Tasks, waiting for 1 second in parallel and finishing).

So the answer is: Mono's impl. of (some) methods is not perfect. In general Task.Delay does not start a thread and even a lot of them should not create multiple threads.

like image 621
Kr0e Avatar asked Feb 19 '14 10:02

Kr0e


1 Answers

On .NET Framework Desktop.

In short, there this special VM thread which periodically checks queue of timers and runs timers' delegates on thread pool queue. Task.Delay does not create new Thread, but still may be heavy, and no guaranties on order of execution or being precise about deadlines. And as I understand, passing cancellation Task.Delay may end up in just removing item from collection, with no thread pool work queued.

Task.Delay scheduled as DelayPromise by creating new System.Threading.Timer. All timers are stored in AppDomain singleton of TimerQueue. Native VM timer used to callback .NET to check if need to fire any timers from queue. Timer delegates scheduled for execution via ThreadPool.UnsafeQueueUserWorkItem.

From performance point of view, it seems better to cancel delay if delay ends earlier:

open System.Threading
open System.Threading.Tasks

// takes 0.8% CPU
while true do
  Thread.Sleep(10)
  Task.Delay(50)

// takes 0.4% CPU
let mutable a = new CancellationTokenSource()
while true do
  Thread.Sleep(10)
  a.Cancel()
  a.Dispose()
  a <- new CancellationTokenSource()
  let token = a.Token
  Task.Delay(50,token)
like image 171
Dzmitry Lahoda Avatar answered Sep 29 '22 22:09

Dzmitry Lahoda