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 Timer
object 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:
Tasks
do not finish at the same time, there are huge delays (probably about 500-600ms)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.
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With