Question: How to Print numbers in sequential manner using TPL multiple tasks

What is the problem with the following code?
I am trying to print numbers in sequence. One Task is for printing odd numbers and the other task is for printing even numbers.
Could somebody help me how to introduce the synchronization, so that I can get sequenced numbers from 0-10?
Code :
    using System.Threading.Tasks;

Task.Run(() =>{

        var printNumbers = new PrintNumbers();


        for(var i =0; i<10; i++)
        {                
                if(i % 2 == 0)
                    Console.WriteLine(i);                
        }
})


Task.Run(() =>{

        var printNumbers = new PrintNumbers();

        for(var i =0; i<10; i++)
        {                
                if(i % 2 != 0)
                    Console.WriteLine(i);                
        }
    }
})



 oddTask.Start();
evenTask.Start();

Task.WhenAll(new Task[] { oddTask, evenTask});

public class PrintNumbers
{
    private static object seqNumbers = new object();    

    public void PrintOddNumber(int number)
    {
        lock(seqNumbers)
        {
            Console.WriteLine(number);
        }
    }

    public void PrintEvenNumber(int number)
    {
        lock(seqNumbers)
        {
            Console.WriteLine(number);
        }
    }
}

Note: I am using Visual Studio Code.


Answer:

What is the problem with the following code?

object seqNumbers = new object();
Task.Run((seqNumbers) =>{   // 1. Task.Run does not allow to pass arguments
        for(var i =0; i<10; i++)
        {
            lock(seqNumbers)  // 2. Using `lock` in here is problematic : 
                              //    you cannot enforce a toggle with a single one.
            {
                if(i % 2 == 0)
                    Console.WriteLine(i);
            }
        }
})
.Wait(); // 3. This will block until the Task is completed

Task.Run((seqNumbers) =>{
    lock(seqNumbers)
    {
        for(var i =0; i<10; i++)
        {
            lock(seqNumbers)
            {
                if(i % 2 != 0)
                    Console.WriteLine(i);
            }
        }
    }
})
.Wait();

So, you see: The locking here is actually futile, because both Tasks run in sequence to each other. If you want Tasks to run in parallel (potentially), you need to start both and then await both.
If you need sequential output without utilizing intermediate data storage of results, then you have to do something awkward like this:
using System;
using System.Threading;
using System.Threading.Tasks;
                    
public class Program
{
    public static async Task Main()
    {
        var oddsem = new SemaphoreSlim(1,1);
        var evensem = new SemaphoreSlim(0,1);
        
        await Task.WhenAll(
            Task.Run(async () => {
                for(int i = 0; i < 10; i++){
                    await oddsem.WaitAsync();
                    try{
                        if( i % 2 == 0 ) Console.WriteLine("A: {0}",i);
                    }
                    finally{evensem.Release();}
                }
            }), 
            Task.Run(async () => {
                for(int i = 0; i < 10; i++){
                    await evensem.WaitAsync();
                    try{
                        if( i % 2 == 1 ) Console.WriteLine("B: {0}",i);
                    }
                    finally{oddsem.Release();}
                }
            })
        );
    }
}

Output:

A: 0
B: 1
A: 2
B: 3
A: 4
B: 5
A: 6
B: 7
A: 8
B: 9

See Fiddle.
Which defeats all the benefits of making this parallel in the first place.