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.