Process:
A process is not a physical entity; it is a computer concept (virtual) that represents the collection of all computing resources (CPU, memory, disk, network, etc.) used when a program is running.
Thread:
Also a computer concept (virtual). It is the smallest execution flow of a process (any operation response requires an execution flow). A thread has its own computing resources and depends on the process.
Multithreading:
Multiple execution flows executing concurrently – that is multithreading.
A process requests threads from the CLR, and the CLR requests them from the operating system.
Thread.CurrentThread.ManagedThreadId – The ID of the current thread. Any operation response requires a thread; this ID is meaningless as it is a CLR-defined identifier.
In .NET history, many multithreading approaches have existed: Thread, ThreadPool, BeginInvoke, Task, Parallel, AwaitAsync. However, any multithreading revolves around delegates, and the best practice is Task.
More threads is not always better – generally, the number of threads should not exceed CPU core count × 3, but this is not absolute. It depends on the specific task each thread executes. I have experienced processing video where increasing threads beyond the core count actually increased overall task completion time compared to keeping threads less than or equal to the core count. In general, if each thread's task consumes little CPU, more threads can be used; if each task is CPU-intensive, adding many threads only increases CPU scheduling overhead.
In practice, it is common to create threads based on task type and strictly control the number of each type, rather than dynamically creating threads arbitrarily. This facilitates thread management and reduces scheduling overhead.
Non-determinism of threads: executing the same task, a thread started earlier does not necessarily finish earlier, and a later one does not necessarily finish later. Why? Because threads are resources managed by the computer. Even if you request a thread first, you may not actually get it before another request. This is unlike sequential code where control is predictable. The difference in execution time for the same task is due to the CPU scheduling policy (time slices).
1. Temporary Variable Issue
When a variable is declared outside a loop and used inside threads created in the loop, the value captured may not be as expected. The common solution is to create a local variable inside the loop so each thread captures a distinct instance.
// Problematic version (variable captured by closure)
for (int i = 0; i < 10; i++)
{
Task.Run(() => Console.WriteLine(i)); // likely prints 10 repeatedly
}
// Correct version: capture a copy
for (int i = 0; i < 10; i++)
{
int temp = i;
Task.Run(() => Console.WriteLine(temp)); // prints 0..9
}
2. Non-determinism
The following example simulates a parking lot: open the door, let people enter and exit, close the door only after everyone has left.
List<Task> taskList = new List<Task>();
for (int i = 0; i < 5; i++)
{
int carId = i;
taskList.Add(Task.Run(() =>
{
Console.WriteLine($"Car {carId} entered.");
Thread.Sleep(new Random().Next(500, 1500));
Console.WriteLine($"Car {carId} exited.");
}));
}
Task.WaitAll(taskList.ToArray());
Console.WriteLine("All cars have left. Close the door.");
Task.WaitAll() blocks the main thread until all awaited tasks complete. There is also Task.WaitAny() which waits for any one task to finish.
If we don't want to block the main thread, one brute-force approach is to wrap the tasks inside another Task.Run(() => { ... }), but that introduces nested threads and is hard to control; avoid it.
A more elegant solution is to use callbacks.
// Attach a callback to a specific task
taskList[4].ContinueWith(t => Console.WriteLine("Car 5 has left. Please note."));
// Or use ContinueWhenAny / ContinueWhenAll
Task.Factory.ContinueWhenAny(taskList.ToArray(), t => Console.WriteLine("Today's first car has left."));
Task.Factory.ContinueWhenAll(taskList.ToArray(), t => Console.WriteLine("All cars have left."));
When ContinueWhenAny and Task.WaitAny appear together, which runs first? It's non-deterministic. The same applies to ContinueWhenAll and Task.WaitAll.
A small trick: if you want to use WaitAll in the main thread for flow control but also ensure order, you can add the continuation task itself into the task list:
var cleaningTask = Task.Factory.ContinueWhenAll(taskList.ToArray(), t => Console.WriteLine("Cleaning done."));
taskList.Add(cleaningTask);
Task.WaitAll(taskList.ToArray());
Console.WriteLine("Door closed.");
This way, WaitAll will wait for the continuation task as well, ensuring cleaning finishes before the door closes. This is a clever pattern often seen in inter-component control.
Handling a Fixed Number of Threads for an Unknown, Unpredictable Number of Tasks
Approach 1: Task Queue
Each thread pulls a task from a queue, executes it, then pulls the next. Pay attention to thread safety of the queue to avoid duplicate task execution.
Approach 2: Using Task.WaitAny
public static void TestThreadPool()
{
List<Task> taskList = new List<Task>();
for (int i = 0; i < 100; i++)
{
var flag = i;
taskList.Add(Task.Run(() =>
{
Console.WriteLine($"TaskStart:{Thread.CurrentThread.ManagedThreadId}, taskid:{flag}");
var r = new Random();
var val = 800 + r.Next(1, 100);
Thread.Sleep(val);
Console.WriteLine($"TaskEnd:{Thread.CurrentThread.ManagedThreadId}, taskid:{flag}, cost:{val}");
}));
if (taskList.Count == 5)
{
Task.WaitAny(taskList.ToArray());
taskList = taskList.Where(t => t.Status == TaskStatus.Running).ToList();
}
}
Console.ReadLine();
}
In this example, we start tasks and keep a fixed set of 5 active threads. Whenever one completes, a new task is started immediately.
Approach 3: Using Parallel.ForEach
public static void TestParallel()
{
List<int> workItems = new List<int>();
for (int i = 0; i < 100; i++)
{
workItems.Add(i);
}
ParallelOptions parallelOptions = new ParallelOptions()
{
MaxDegreeOfParallelism = 5
};
Parallel.ForEach(workItems, parallelOptions, work =>
{
Console.WriteLine($"TaskStart:{Thread.CurrentThread.ManagedThreadId}");
var r = new Random();
var val = 800 + r.Next(1, 100);
Thread.Sleep(val);
Console.WriteLine($"TaskEnd:{Thread.CurrentThread.ManagedThreadId}, cost:{val}");
});
Console.ReadLine();
}
Parallel.ForEach is a more specialized and convenient tool for such parallel loops, managing the degree of concurrency automatically.