Threads (And Tasks) run pseudo parallelly

Only one thread runs at any moment

Processors of the past (or what is called as a core) are capable of executing only one instruction at a time. Consequently, only a single thread could be actively running at any given moment. This is due to the architecture of processor and presence of having specialized one set of registers called as stack pointer and program counter.

Operating system magic

The operating system is responsible for creating the illusion of parallel execution for the user. It achieves this by running each thread for a brief time slice, typically in the range of a few milliseconds. Through this process, each thread advances, creating the perception of simultaneous execution for the human user.

Processor Imrpovements

Over time, there have been significant improvements in processors. Previously, a single chip typically contained only one processor or core. However, modern processors commonly feature multiple cores, allowing for the concurrent execution of multiple threads.

Hyper Threading by Intel

Intel introduced a significant advancement in this domain with its Hyper-Threading technology. This innovation involves creating cores capable of executing multiple threads simultaneously. Referred to as Hyper-Threading, this technology enables a core to handle multiple threads concurrently, thereby increasing its efficiency. The number of threads a core can run simultaneously is the number of logical cores said to be present in the given physical core.

Experimenting with code

The maximum number of threads truly running in parallel is equivalent to the total count of logical cores across all physical cores. Yet, thanks to the scheduling wizardry of the operating system, the effective number of threads running pseudo-parallel can be significantly higher.

The below C# prints the number of physical core and number of logical cores running the code. The code spawns up couple of Tasks. Each task prints the logical processor id 2 times with a delay in the middle.

Note the code needs System.Management nuget package

using System.Management;
using System.Runtime.InteropServices;
using System.Security;

[DllImport("Kernel32.dll"), SuppressUnmanagedCodeSecurity]
static extern int GetCurrentProcessorNumber();

foreach (var item in new ManagementObjectSearcher("Select * from Win32_Processor").Get())
{
    Console.WriteLine("Number Of Physical Core: {0}", item["NumberOfCores"]);
    Console.WriteLine("Number Of Logical Core: {0}", item["NumberOfLogicalProcessors"]);
}

List<Task> tasks = new List<Task>();

for(int i = 0; i < 14; i++)
{
    var taskI = i;
    tasks.Add(Task.Run(() => ThreadMethod(taskI)));
}

Task.WaitAll(tasks.ToArray());

void ThreadMethod(int threadId)
{ 
    for(int i = 0; i < 2; i++)
    {
        Console.WriteLine($"The Thread id:{threadId.ToString().PadLeft(3)} .... The processor id:{GetCurrentProcessorNumber().ToString().PadLeft(3)}");
        for(int x = 0; x < 100000; x++)
        {
            for (int y = 0; y < 10000; y++)
            {
            }
        }
    }
}

Code Output

From the output we can observe the following

  • The system has 14 physical cores and 20 logical cores

  • The thread with identifier 2 started in logical processor 10 and then ran in 18

  • The thread with identifiers 6 and 7 run in logical processor 1

  • From this we know there is no thread to processor affinity or exclusivity.