I’ve created this series to help any upcoming engineers in refreshing their knowledge and dwell deeper into their understanding of computation. I hope you find the series useful.
Today we are talking about threads..
What is a Thread?
By simple definition, “A thread is an execution context. Contains all of the necessary information a CPU needs to execute a stream of instructions”.
"Suppose you're reading a book, and you want to take a break right now, but you want to be able to come back and resume reading from the exact point where you stopped. One way to achieve that is by jotting down the page number, line number, and word number. So your execution context for reading a book is these 3 numbers. If you have a roommate, and she's using the same technique, she can take the book while you're not using it, and resume reading from where she stopped. Then you can take it back, and resume it from where you were."
pwnall
Let us also understand the following terminologies..
Task – A general unit of work, which can be represented by a process, a thread, or a unit of asynchronous work depending on the context.
Process – a process is an instance of program that is being executed. It is the highest level unit of work that the operating system manages. Each process has its own memory space and resources (like file descriptors, environment variables, etc.)
More about processes..
- A process has its own memory (address space), so each process is isolated from others.
- Processes can communicate with each other.
- A process can contain multiple threads
- Creating a process is relatively expensive because it requires allocating a large amount of system resources.
- When a process terminates, all of its threads terminate as well.
Example
Running a browser of a text editor, where each instance is its own process.
More about tasks..
- In the context of operating systems, a task could refer to a process or thread that is being scheduled by the operating system.
- In asynchronous programming or concurrent programming, a task refers to a discrete piece of work that can be executed concurrently, often abstracted by ‘async/await’ , a task might not map directly to an OS thread but instead be a scheduled operation on a thread pool.
What the hell is a thread pool?
The thread pool creates a number of threads (fixed or dynamic) beforehand. This helps avoid the overhead of creating and destroying threads frequently, which can be costly in terms of system resources.
When tasks are submitted, they are placed in a queue. Each available thread in the pool will take a task from the queue, execute it, and then return to the pool, ready for the next task.
The thread pool efficiently manages the limited number of threads, distributing work among them. It prevents the system from being overwhelmed by too many threads being created or destroyed in rapid succession.
A thread pool allows multiple tasks to be executed concurrently, but limits the number of threads actually running at any given time. This helps balance resource usage and maintain system performance.
Now that we understood what is under the hood, modern C# programming allows us to use a higher level of abstraction in regards to threads, making it easier to manage asynchronous and parallel programming.
How does the data stays consistent? If two tasks are within the same thread or process..
Well then they will share the same memory space, so we will need to use synchronization techniques to prevent conflicts.
Locks and Synchronization Primitives
Mutex
A mutex (mutual exclusion) is a locking mechanism that ensures only one thread can access a critical section of a code at a time. When one thread locks a mutex, no other thread can access that portion of code until the mutex is unlocked.
Example
Two threads are trying to update the same variable use a mutex to lock the variable, ensuring that only one thread can modify it at a time.
Semaphores
A semaphore controls access to a resource with a counter. It allows multiple threads to access a limited resource but restricts the maximum number of concurrent accesses.
Example
If you have a limited pool of database connections, a semaphore can allow up to ‘n’ tasks to use the pool concurrently, but block any additional tasks until the connection is freed.
Atomic Operations
Certain operations can be made atomic, meaning they are executed as a single, indivisible step, preventing any other thread from interrupting them.
Example
Incrementing a counter in an atomic operation ensures that no thread can see an intermediate value with the counter is being updated.
Condition Variables
Condition variables allow threads to wait until certain conditions are met before proceeding. This can help coordinate access to shared resources between tasks
Immutable Data
If data is immutable, then concurrent tasks can safely consume it without any risk of modifying it. Immutable data structures help prevent data corruption because they can’t be altered.
Transaction Based Systems
In database systems, or systems that require transactional integrity, operations are executed as ‘transactions’. A transaction ensures that a set of operations either all succeed or all fail, maintaining the consistency of data even when multiple transactions are running concurrently.
Short summary about Transactions
Let us better understand what is a ‘transaction’..
A transaction is a sequence of operations treated as a single logical unit of work. The key properties of a transaction are known as ACID:
- Atomicity – All operations must succeed or fail as a whole
- Consistency – Transitions the system from one valid state to another
- Isolation – Transactions do not interfere with each other
- Durability – Once committed, the results are permanent.
Imagine a bank transferring money between accounts. Both debit and credit operations must succeed for the transaction to be valid, any further transactions will wait for the first one to succeed or fail, thus ensuring a singular state. Whereby the first transaction moves the state of the system (in our case, both accounts) from state A to state B or from state A to state A if it failed, and once it is completed, the results are permanent.
Concurrency Control Protocols
In systems that require high concurrency, like databases or distributed systems, specialized concurrency control mechanisms are used to avoid conflicts.
Concurrency, as we covered already, is the process of doing multiple units of work at the same time. Poor engineering can result in, for example, two functions changing the same database field at the same time, creating a problem of overwriting or deleting the data. Same for distributed systems, where the distributed system needs to share a singular state across multiple nodes (computers) around the world, as different requests alter the supposedly same state, they can result in different states across different nodes and we are again – facing the same problem of data corruption.
Let us see what we can do in such cases..
Optimistic Concurrency Control
Assumes that conflicts between tasks are rare, so tasks are allowed to execute without restrictions. Before committing changes, the system checks for conflicts. If a conflict is detected, the task is rolled back and retired.
Example
In a web application, two users might try to update the same record. With OCC, the system allows both updates to proceed but checks if the data was modified before the task started. If so, one update will fail and be retried.
Pessimistic Concurrency Control
Assumes that conflicts are likely, so tasks are locked to prevent other tasks from accessing the resource until the first task completes.
Example
In a banking system, if one task is updating an account balance, it will lock the account so that no other task can modify it until the operation is finished.
Pessimistic Control
Concurrency Optimized Data Structures
Modern programming languages and frameworks provide thread-safe data structures, which are designed to handle concurrent modifications without the need for explicit locks.
Example
A ConcurrentHashMap in Java, allows multiple threads to read and write to the map without needing explicit locks.
Asynchronous and non-blocking designs
Some systems, like Node.js, use asynchronous or non-blocking models to avoid conflicts altogether by scheduling tasks in such a way that they don’t interfere with each other.
Example
In event-driven systems, like node.js, tasks run asynchronously and use callbacks or promises to ensure that shared-data is not accessed or modified concurrently. Its also worth noting, that the ‘Event Loop’ is utilized in the architecture of node.js to prevent it from blocking code when running extensive tasks, but that requires its own article and is beyond the scope of this one.
To conclude, its worth noting..
- Typescript/Javascript/Node.js don’t require pessimistic or optimistic locking mechanisms since they operate in a single-threaded environment by design. They use an event-driven model with a single execution thread to handle all asynchronous events. This single-threaded nature avoids many of the traditional concurrency issues we may find in C# or Rust.
- No true Parallelism in TS/JS, the tasks in those programming languages don’t run concurrently on multiple threads, therefore there is no need to use concurrency control mechanisms like locks.
- Clarifying “Atomicity” in TS/JS – Typescript doesn’t require true atomicity due to its single-threaded nature, if the code was running in a scenario involving multiple async updates, there could still be a logical race condition if multiple increments we’re triggered before the first one completes. Therefore, in a single natured environment, if we write our async code properly and without any logical errors that could potentially corrupt our data, we are safe. If multiple async increments are expected, using some form of queuing or locking would make sense.
- In C#, the ‘Task’ class uses the ThreadPool to manage the execution of the task. The ThreadPool dynamically manages the number of threads based on the system load, CPU availability and the number of tasks being executed. It uses the ‘work-stealing’ algorithm to optimize load balancing across multiple threads, if one thread finishes its tasks early, it can ‘steal’ tasks from other threads that are still busy which helps to maximize CPU utilization and reduce idle time.