1

My code puts 10 'jobs' in a queue, displays their thread IDs, and start running right away and in parallel. I know the jobs are running in parallel since each job is simply a 20 second delay, and all 10 jobs complete in 20 seconds. What perplexes me is that there are several duplicate ThreadIDs, and supposedly each thread should have a unique ID from what I have read. How is this possible? Are there duplicates because the duplicates could be on different processor cores (if so this would not be great as eventually I want to be able to cancel a task using its thread ID) ?

Here's a list of the thread IDs that were shown on my console window"

Thread ID: 10 Thread ID: 11 Thread ID: 11 Thread ID: 12 Thread ID: 13 Thread ID: 14 Thread ID: 15 Thread ID: 16 Thread ID: 6 Thread ID: 6

I simplified the code as much as I could, and timed how long it took the program to finish.

This is a console app

class Program
{

    private static Object lockObj = new Object();

    static void Main(string[] args)
    {
        var q = new TPLDataflowMultipleHandlers();
        var numbers = Enumerable.Range(1, 10);

        Console.Clear();

        foreach (var num in numbers)
        {
            q.Enqueue(num.ToString());
        }
        Console.ReadLine();
    }
} // end of program class


public class TPLDataflowMultipleHandlers
{
    private static Object lockObj = new Object();

    private ActionBlock<string> _jobs;

    public TPLDataflowMultipleHandlers()
    {
        var executionDataflowBlockOptions = new ExecutionDataflowBlockOptions()
        {
            MaxDegreeOfParallelism = 16,
        };

        _jobs = new ActionBlock<string>(async (job) =>
       {
           ShowThreadInformation("Main Task(Task #" + Task.CurrentId.ToString() + ")");
           Console.WriteLine($"STARTING job:{job},  thread: { Thread.CurrentThread.ManagedThreadId}");

           await Task.Delay(20000);

           Console.WriteLine($"FINISHED job:{job},  thread: { Thread.CurrentThread.ManagedThreadId}");
       }, executionDataflowBlockOptions);
    }

    public void Enqueue(string job)
    {
        _jobs.Post(job);
    }

    private static void ShowThreadInformation(String taskName)
    {
        String msg = null;
        Thread thread = Thread.CurrentThread;
        lock (lockObj)
        {
            msg = String.Format("{0} thread information\n", taskName) +
                  String.Format("   Background: {0}\n", thread.IsBackground) +
                  String.Format("   Thread Pool: {0}\n", thread.IsThreadPoolThread) +
                  String.Format("   Thread ID: {0}\n", thread.ManagedThreadId);
        }
        Console.WriteLine(msg);
    }
}

I was fully expecting 10 unique thread ID numbers.

PMF
  • 14,535
  • 3
  • 23
  • 49
  • 1
    The way you're doing a 20-second delay, you're awaiting it which means you've yielded to other paths of execution that can run on the same thread. – madreflection Nov 01 '19 at 19:26
  • Your example doesn't even call the `ShowThreadInformation` method anywhere. – PMF Nov 01 '19 at 19:28
  • That ActionBlock doesn't do anything useful, it merely gets 16 async tasks started. Which are executed by the threadpool scheduler, it keeps the number of active threads limited to the number of cores. Mixing is not a great idea. – Hans Passant Nov 01 '19 at 19:28
  • Well, don't I feel dumb. That makes perfect sense. I re-ran the program above with thread.Sleep(20000) instead, and got 20 unique thread ID numbers. – PastExpiry.com Nov 01 '19 at 19:30
  • @PMF it is called right after the line: _jobs = new ActionBlock(async (job) => – PastExpiry.com Nov 01 '19 at 19:34
  • @ Hans Passant How is it limitted to the number of cores? I set the MaxDegreeofParallelism to 16. – PastExpiry.com Nov 01 '19 at 19:36

1 Answers1

5

Threads and Tasks are not the same things - think of a Thread as a worker, and a Task as a piece of work to be performed. Just because you created 10 things that need to be done doesn't mean that you need 10 workers to do them - it would be much more efficient if, say, the 4 workers you already have (the default amount of worker threads in the .NET ThreadPool) started executing the work units, and new workers (Threads) would be created only if the existing ones don't seem to keep up. Each "work unit" you created in your code is very short, and so it gets executed very quickly and the same thread that performed it becomes free and runs another one that was waiting in a queue in the background.

If you want to see this in action, just place something like Thread.Sleep(30000) somewhere in your ShowThreadInformation. This will cause the execution of your tasks to be artificially long, and the .NET thread pool will notice tasks are being starved in the queue and spin up new threads to execute them.

Take a look here - What is the difference between task and thread?

Nimrod Dolev
  • 577
  • 3
  • 8
  • I changed task.delay to thread.sleep just before I saw your answer, and got the 10 unique thread IDs that I was expecting. I thought I read somewhere though that if you do not set the MaxDegreeofParallelism to some number that it defaults to 1. Are you saying that I do not even need to declare the MaxDegreeofParallelism, and that the thread pool will take care of it (even if it has to create 100 threads for 100 long jobs)? – PastExpiry.com Nov 01 '19 at 19:44
  • 1
    The `MaxDegreeofParallelism` defines the number of tasks that the `DataflowBlock` will have executing in parallel - it's not regulating the number of tasks that can be executed by the global .NET `ThreadPool`, just the amount of tasks that the `ActionBlock` you're creating will execute. If you define the `MaxDegreeofParallelism` on the block as 16, and your tasks are long, there will be 16 tasks created for the first 16 items you push into the Block, but the 17th will wait. This is a level above the `ThreadPool`, which can limit the actual parallelism by itself, globally. – Nimrod Dolev Nov 02 '19 at 20:12