Mutex(lock) with C# example
In previous blog posts, we've observed that Threads/Tasks have the capability to run concurrently (more accurately, pseudo-concurrently). Additionally, we've explored how it's the responsibility of the operating system to manage thread scheduling. There is a common problem when multiple threads/tasks are accessing a common resource.
Problem
Consider a scenario where we have two tasks: one generates a star pattern in the console, while the other produces a hash pattern. Since the operating system handles task scheduling, we lack control over the order in which tasks execute. Consequently, during the creation of a pattern, interruption may occur, resulting in the drawing of an interleaved star and hash pattern. Such unpredictability is not aligned with our intentions.
Problem Demonstration
The below code prints star and hash patterns.
PrintStartPattern prints start pattern
PrintHashPattern prints hash pattern
These two methods are run from 2 different tasks
var t1 = Task.Run(() => {
for(int i = 0; i < 3; i++) {
PrintStarPattern();
}
});
var t2 = Task.Run(() => {
for (int i = 0; i < 3; i++) {
PrintHashPattern();
}
});
Task.WaitAll(t1, t2);
void PrintStarPattern() {
for (int i = 0; i < 3; i++) {
for (int j = i; j < 3; j++) {
Console.Write("*");
Delay();
}
Console.WriteLine();
}
Console.WriteLine();
}
void PrintHashPattern() {
for (int i = 0; i < 3; i++) {
for (int j = i; j < 3; j++) {
Console.Write("#");
Delay();
}
Console.WriteLine();
}
Console.WriteLine();
}
void Delay() {
for (int i = 0; i < 10000; i++) {
for (int j = 0; j < 10000; j++) {
}
}
}
Output
- The output shows the star and the hash pattens are interleaved. Making neither pattern clear
Solution
The above problem could be solved by mutual exclusion or locks. In C#, Mutexes (or locks) play a vital role. A thread can secure a lock on a resource to utilize it and then release it when done. Programmers in the system follow this practice of locking before accessing a resource and unlocking after accessing the resource.
The lock, established by .NET utilizing the operating system, ensures that once a task secures a lock, no other task can acquire it until the first one releases it. Any task attempting to lock a locked resource will move to a waiting state.
Code fix by lock
The below code solves the problem
consoleLock is an object
This object for the purposes of our program represents the resource "Console"
The task locks the object before accessing the console and unlocks the object after accessing the console
Object consoleLock = new object();
var t1 = Task.Run(() => {
for(int i = 0; i < 3; i++) {
lock(consoleLock) {
PrintStarPattern();
}
}
});
var t2 = Task.Run(() => {
for (int i = 0; i < 3; i++) {
lock(consoleLock) {
PrintHashPattern();
}
}
});
Task.WaitAll(t1, t2);
void PrintStarPattern() {
for (int i = 0; i < 3; i++) {
for (int j = i; j < 3; j++) {
Console.Write("*");
Delay();
}
Console.WriteLine();
}
Console.WriteLine();
}
void PrintHashPattern(){
for (int i = 0; i < 3; i++) {
for (int j = i; j < 3; j++) {
Console.Write("#");
Delay();
}
Console.WriteLine();
}
Console.WriteLine();
}
void Delay() {
for (int i = 0; i < 10000; i++) {
for (int j = 0; j < 10000; j++) {
}
}
}
Fixed output
- The output after fixing the code shows start patterns and hash patterns clearly separated
Conclusion
In this blog we have understood the problem of accessing shared resources from different tasks/threads. And we have also seen how to solve them with lock in C#