You do two things sequentially:
(1) start task 1.
(2) start task 2.
Now starting a task, i.e. executing Task.Run()
, is slow and expensive. It takes perhaps, say, 5ms. Running that very short task takes perhaps only 1ms. So task 1 is finished long before task 2 has started.
Time -> ...1ms...1ms...1ms...1ms...1ms...1ms..........1ms............1ms...1ms...1ms...1ms...1ms...
main [set up task 1..................................][set up task 2............................................][main ends]
task1 [DoWork() starts] [DoWork() ends]
task2 [DoWorkToo() starts] [DoWorkToo() ends]
If you want to have the tasks run in parallel you must make their runtime long in comparison to the task startup time. One solution is simply to run the loop many thousands of times, but that can be unwieldy. Better is to let the tasks sleep during loop execution so that task1 is still running when task 2 starts.
Here is an example with longer running tasks showing nicely how the execution interleaves:
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics; // stopwatch
namespace TwoTasks2
{
class Program
{
static Stopwatch globalStopWatch = new Stopwatch();
private static void DoWork()
{
Console.WriteLine("========================= Entering DoWork() after " + globalStopWatch.ElapsedMilliseconds);
int sum = 0;
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 100000; i++)
{
if (i % 10000 == 0)
{
Console.WriteLine("Task one " + 10000 + " cycles took " + sw.ElapsedMilliseconds + " ms");
sw.Stop();
sw.Start();
Console.WriteLine("Thread ID: " + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("CPU ID: " + Thread.GetCurrentProcessorId());
}
}
}
private static void DoWorkToo()
{
Console.WriteLine("========================= Entering DoWorkToo() after " + globalStopWatch.ElapsedMilliseconds);
int sum = 0;
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 100000; i++)
{
if (i % 10000 == 0)
{
Console.WriteLine(" Task two " + 10000 + " cycles took " + sw.ElapsedMilliseconds + " ms");
sw.Stop();
sw.Start();
Console.WriteLine(" Thread ID: " + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(" CPU ID: " + Thread.GetCurrentProcessorId());
}
}
}
public static void Main()
{
globalStopWatch.Start();
var task1 = Task.Run(() => DoWork());
long ms = globalStopWatch.ElapsedMilliseconds;
Console.WriteLine("--------------------- RunTask 1 took " + ms);
var task2 = Task.Run(() => DoWorkToo());
Console.WriteLine("--------------------- RunTask 2 took " + (globalStopWatch.ElapsedMilliseconds-ms));
var tasks = new Task[] { task1, task2 };
Task.WaitAll(tasks);
}
}
}
Example output on my machine, Debug build:
--------------------- RunTask 1 took 23
========================= Entering DoWork() after 39
--------------------- RunTask 2 took 18
Task one 10000 cycles took 0 ms
Thread ID: 4
========================= Entering DoWorkToo() after 41
Task two 10000 cycles took 0 ms
Thread ID: 5
CPU ID: 1
CPU ID: 2
Task two 10000 cycles took 1 ms
Thread ID: 5
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID: 4
CPU ID: 1
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID: 4
Task two 10000 cycles took 2 ms
Thread ID: 5
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID: 4
CPU ID: 1
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID: 4
Task two 10000 cycles took 2 ms
Thread ID: 5
CPU ID: 1
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID: 2
Task two 10000 cycles took 2 ms
Thread ID: 5
CPU ID: 1
Task one 10000 cycles took 3 ms
Thread ID: 4
CPU ID: 2
Task two 10000 cycles took 2 ms
Thread ID: 5
CPU ID: 1
Task one 10000 cycles took 3 ms
Thread ID: 4
CPU ID: 2
Task two 10000 cycles took 2 ms
Thread ID: 5
CPU ID: 1
Task one 10000 cycles took 3 ms
Thread ID: 4
CPU ID: 2
Task two 10000 cycles took 2 ms
Thread ID: 5
CPU ID: 3
Task one 10000 cycles took 3 ms
Thread ID: 4
CPU ID: 4
Task two 10000 cycles took 2 ms
Thread ID: 5
CPU ID: 10
Task two 10000 cycles took 3 ms
Thread ID: 5
CPU ID: 1
A Release build is much orderlier:
--------------------- RunTask 1 took 21
========================= Entering DoWork() after 37
--------------------- RunTask 2 took 16
Task one 10000 cycles took 0 ms
Thread ID: 4
CPU ID: 4
Task one 10000 cycles took 0 ms
Thread ID: 4
CPU ID: 6
Task one 10000 cycles took 1 ms
Thread ID: 4
CPU ID: 2
Task one 10000 cycles took 1 ms
Thread ID: 4
CPU ID: 2
Task one 10000 cycles took 1 ms
Thread ID: 4
CPU ID: 3
Task one 10000 cycles took 1 ms
Thread ID: 4
CPU ID: 6
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID: 2
========================= Entering DoWorkToo() after 39
Task two 10000 cycles took 0 ms
Thread ID: 5
CPU ID: 11
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID: 10
Task two 10000 cycles took 0 ms
Thread ID: 5
CPU ID: 11
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID: 10
Task two 10000 cycles took 0 ms
Thread ID: 5
CPU ID: 11
Task one 10000 cycles took 2 ms
Thread ID: 4
CPU ID: 10
Task two 10000 cycles took 0 ms
Thread ID: 5
CPU ID: 1
Task two 10000 cycles took 0 ms
Thread ID: 5
CPU ID: 1
Task two 10000 cycles took 0 ms
Thread ID: 5
CPU ID: 11
Task two 10000 cycles took 0 ms
Thread ID: 5
CPU ID: 11
Task two 10000 cycles took 0 ms
Thread ID: 5
CPU ID: 11
Task two 10000 cycles took 0 ms
Thread ID: 5
CPU ID: 1
Task two 10000 cycles took 1 ms
Thread ID: 5
CPU ID: 1
It takes a whopping 35ms until the first task is running! That's eternity on modern CPUs. The second task then starts much faster.
The tasks take turns even between printing lines to the console. You can also see that even the same thread is hopping from core to core as Windows sees fit. (That actually surprised me. My Ryzen has 6 real cores, and there isn't any significant load, so I'd leave the tasks running where they are.)