-1

I have some very simple code that's attempting to multi-thread an existing script.

On inspecting the treads window in visual Studio and calling Thread.CurrentThread.ManagedThreadId it always reports back as the same thread as starting the process. When ending it reports back a different thread id.

The threads do seem to be performing the task asynchronously, but the logging and output from visual studio are making me think otherwise.

Please could someone clarify what is going on and if I've made a mistake in my approach?

namespace ResolveGoogleURLs
{
    class Program
    {
        public static void Main(string[] args)
        {
            HomeController oHC = new HomeController();
        }
    }
}

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace ResolveGoogleURLs
{
    class HomeController
    {
        public static int MaxJobs = 5;
        public static int RecordsPerJob = 1000;

        public static List<Task> TaskList = new List<Task>(MaxJobs);


        public HomeController()
        {
            CreateJobs();

            MonitorTasks();
        }


        public void MonitorTasks()
        {
            while (1 == 1)
            {
                Task.WaitAny(TaskList.ToArray());

                TaskList.RemoveAll(x => x.IsCompleted);

                Console.WriteLine("Task complete! Launching new...");
                CreateJobs();
            }
        }


        public async Task CreateJob()
        {
            Console.WriteLine("Thread {0} - Start", Thread.CurrentThread.ManagedThreadId);

            // read in results from sql

            await Task.Delay(10000);
            Console.WriteLine("Thread {0} - End", Thread.CurrentThread.ManagedThreadId);
        }


        public void CreateJobs()
        {
            while (TaskList.Count < MaxJobs)
            {
                TaskList.Add( CreateJob() );
            }
        }
    }
}

Output:

> Thread 1 - Start
Thread 1 - Start
Thread 1 - Start
Thread 1 - Start
Thread 1 - Start
Thread 4 - End
Thread 5 - End
Thread 4 - End
Thread 6 - End
Thread 8 - End
Task complete! Launching new...
Thread 1 - Start
Thread 1 - Start
Thread 1 - Start
Thread 1 - Start
Thread 1 - Start
Thread 7 - End
Thread 6 - End
Thread 5 - End
Thread 4 - End
Thread 8 - End
Task complete! Launching new...
Thread 1 - Start
Thread 1 - Start
Thread 1 - Start
Task complete! Launching new...
Thread 1 - Start
Thread 1 - Start
Thread 10 - End
Thread 9 - End
Task complete! Launching new...
Thread 1 - Start
Thread 7 - End
Thread 4 - End
Thread 6 - End
Task complete! Launching new...
Thread 1 - Start
Thread 1 - Start
Task complete! Launching new...
Thread 1 - Start
Thread 1 - Start
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
atoms
  • 2,993
  • 2
  • 22
  • 43
  • 1
    `code that's attempting to multi-thread` - no, it's [not](https://stackoverflow.com/q/17661428/11683) attempting that. `Same thread as starting the process. When ending it reports back a different thread id` - because in a console application, the continuation [does not have](https://stackoverflow.com/q/55845696/11683) to run on the original thread. `seem to be performing the task asynchronously, but the logging and output from visual studio are making me think otherwise` - please see https://stackoverflow.com/q/37419572/11683 and https://blog.stephencleary.com/2013/11/there-is-no-thread.html. – GSerg Jul 09 '19 at 15:45
  • It's not clear how the result you're getting differs from the result you're expecting. If I'm not mistaken, isn't that the question - why am I getting this, *not* that? What is the expected result? My expectation would be that which thread starts the task and which thread finishes it would be somewhat unpredictable, which is how this output looks. – Scott Hannen Jul 09 '19 at 15:46
  • 2
    tasks != threads; this looks entirely correct and expected to me; what do *you* expect it to look like, and why? – Marc Gravell Jul 09 '19 at 15:48
  • You are working with tasks. A task is not equivalent to a thread. The system will execute a task there on any available thread that's available to it, simplified speaking. Any code in an async method following an `await`ed operation might be executed in any arbitrary thread context, that might not even be background thread from the thread pool, and this depends entirely on the behavior/nature of the awaited operation... –  Jul 09 '19 at 15:48
  • 1
    Doing async is _not_ synonymous with creating threads. It's mainly about avoiding unnecessary threads. – H H Jul 09 '19 at 15:49
  • Also note that any code in your `CreateJob()` method before the first `await` statement is executed **synchronously** in the thread that is calling the method. Only the awaited operation and any remaining code after the `await` expression will/can be executed in some other thread context... –  Jul 09 '19 at 15:50
  • On top of that, you are running `CreateJob` in a [fire and forget](https://stackoverflow.com/q/18502745/11683) way. – GSerg Jul 09 '19 at 15:51
  • [Best article I've read on this subject](https://blog.stephencleary.com/2013/11/there-is-no-thread.html). I am not one of the smart people who got this intuitively. This helped me a lot. – Scott Hannen Jul 09 '19 at 15:53
  • Thank you all for your very helpful comments! I was hoping to parallel a slow running task, creating 5 threads to process batches of 1000 objects each. Once a thread finishes I would like to start another with a new batch of 1000. Will have a good read @ScottHannen thank you. – atoms Jul 09 '19 at 16:11
  • @ScottHannen brilliant article was very helpful. With this and GSerg links I have a good understanding of what happened and why. Thank you all so much for your time. – atoms Jul 12 '19 at 08:29

1 Answers1

0

Tasks (class Task) are not the same as threads (class Thread).

Thread can be imagined as a virtual CPU that can run its code in same time than other threads. Every thread has its own stack (where local variables and function parameters are stored). In .NET a thread is mapped into a native thread - supported by the platform (operation system), that has things like thread kernel object, kernel mode stack, thread execution block, etc.. This makes thread a pretty heavy-weight object. Creation of new thread is time consuming and even when threads sleeps (doesn't execute its code), it still consumes a lot of memory (thread stack).

The operation system periodically goes through all running threads and based on their priority it assigns them a time slot, when a thread can use the real CPU (execute its code).

Because threads consume memory, they are slow to create and running too many threads hurts overall system performance, tasks (thread pools) were invented.

Task internally uses threads, because threads are the only way how to run code in parallel way in .NET. (Actually it's the only way how to run any code.) But it does it in efficient way.

When a .NET process is started, internally it creates a thread pool - a pool of threads that are used to execute tasks.

Task library is implemented in that way that when a process is started, a thread pool contains only a single thread. If you start to create new tasks, they are stored into a queue from which that single thread takes and execute one task after another. But .NET monitors if that thread is not overloaded with too many tasks - situation when tasks are waiting 'too long' in a thread pool queue. If - based on its internal criteria - it detects that initially created thread is overloaded, it creates a new one. So thread pool now has 2 threads and tasks can be ran on 2 of them in parallel way. This process can be repeated, so if a heavy load is present, thread pool can have 3, 4 or more threads.

On the other hand, if the frequency of new tasks drops down, and thread pool threads don't have tasks to execute, after 'some time' (what is 'some time' is defined by internal thread pool criteria) thread pool can decide to release some of threads - to conserve system resources. Number of thread pool threads can drop down to initially created single thread.

So task library internally uses threads, but it tries to use them in efficient way. Number of threads is scaled up and down based on the number of tasks program wants to execute.

In almost all cases tasks should be preferred solution to using 'raw' threads.

Timmy_A
  • 1,102
  • 12
  • 9
  • @atoms This answer is wrong and misleading. Tasks are *not* thread pools, which you can clearly see by visiting the links provided in the comments under your question. – GSerg Jul 09 '19 at 18:57
  • Tasks are not thread pools. Tasks are light-weighted objects that allow to use thread pool. – Timmy_A Jul 10 '19 at 00:54
  • Here is a quote from MS documentation of Task class. Because the work performed by a Task object typically executes asynchronously on a thread pool thread rather than synchronously on the main application thread, you can use the Status property, as well as the IsCanceled, IsCompleted, and IsFaulted properties, to determine the state of a task. – Timmy_A Jul 10 '19 at 01:13
  • `Because threads consume memory, they are slow to create and running too many threads hurts overall system performance, tasks (thread pools) were invented.` - this is wrong. `Task internally uses threads, because threads are the only way how to run code in parallel way in .NET` - this is even more wrong. Please read the links provided in the comments to the question, starting from https://blog.stephencleary.com/2013/11/there-is-no-thread.html. – GSerg Jul 10 '19 at 07:06
  • I read that article. But you took it to literally. There are threads. Every multitask OS is based on threads. What author wants to say - and I fully agree with him, you should always use tasks to perform IO operations. But it' up to threads to execute task code. BTW - device drivers are allowed to block threads. Non arbitrary threads created by driver itself. For more detailed explanation please see Chapter 5 of excellent book Programming Windows Drivers Model written by Walter Oney. He did a great explanation there how IO operations are implemented on the kernel level. – Timmy_A Jul 10 '19 at 12:12
  • There are threads. No one denies existence of threads. Threads exist. They exist and they have nothing to do with Tasks. Your entire answer is dedicated to what threads are, what stack they have etc. All that information may be correct, but it is irrelevant to Tasks. The portions of your answer that equate threads to tasks are wrong like I noted above. So your answer consists of two main parts: irrelevant information and wrong information. – GSerg Jul 10 '19 at 15:57
  • No, it isn't. Tasks internally use thread pools. See quotation from MS documentations for class Task. 'Because the work performed by a Task object typically executes asynchronously on a thread pool thread rather than synchronously on the main application thread, you can use the Status property, as well as the IsCanceled, IsCompleted, and IsFaulted properties, to determine the state of a task.' Link is here - https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=netframework-4.8 – Timmy_A Jul 10 '19 at 19:17
  • You are missing a crucial word, *typically*. A thread pool is one possible way to carry out the task's job. It is a possible implementation detail rather than what a task is. And arguably, that article is wrong too in saying "typically". Arguably, typically it is [the opposite](https://stackoverflow.com/q/17661428/11683). – GSerg Jul 10 '19 at 19:33
  • Another quotation from MSDN: 'Behind the scenes, tasks are queued to the ThreadPool, which has been enhanced with algorithms that determine and adjust to the number of threads and that provide load balancing to maximize throughput. This makes tasks relatively lightweight, and you can create many of them to enable fine-grained parallelism.'. https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/task-based-asynchronous-programming A word typically is used there because in future, MS can change its implementation of task library. – Timmy_A Jul 10 '19 at 19:47
  • You are confusing TPL with async/await. Please see https://stackoverflow.com/q/10285159/11683. – GSerg Jul 10 '19 at 21:23
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/196292/discussion-between-timmy-a-and-gserg). – Timmy_A Jul 10 '19 at 22:04