Object locking in C# is mainly employed to control access to a shared resource within a multithreaded setting, guaranteeing that only a single thread can interact with the resource at any given moment. This serves to avert race conditions, which arise when numerous threads try to alter a shared resource simultaneously, resulting in unforeseeable outcomes.

Advantages of Object Locking

  1. Maintaining Data Integrity: Guarantees that the data being accessed or altered by various threads stays reliable and accurate.
  2. Ensuring Thread Safety: Averts multiple threads from running critical code sections concurrently, thereby preventing potential concurrency problems.
  3. Preventing Deadlocks: Through the effective implementation of locking mechanisms, you can steer clear of scenarios where threads are stuck waiting endlessly for each other to release locks.

When to Acquire and Release an Object?

In C#, the lock statement is utilized to obtain the lock on an object and automatically release it upon exiting the code block.

(a) Acquiring an Object

(b) Releasing an Object

Real Scenario

To lock and unlock an object, follow the steps below.

Step 1. Create a file by the name of “CustomLogger.cs” like below.

using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace LockUnlockExample
{
    internal class CustomLogger
    {
        private static readonly object _lockObj = new object();
        private readonly string _logFilePath;

        public CustomLogger(string logFilePath)
        {
            _logFilePath = logFilePath;
        }

        public void LogToFile(string message)
        {
            lock (_lockObj)
            {
                using (StreamWriter writer = new StreamWriter(_logFilePath, true))
                {
                    writer.WriteLine($"{DateTime.Now}: {message}");
                    System.Threading.Thread.Sleep(2000);
                }
            }
        }
        // Explanation of the above code:
        // 1. The lock statement ensures that the code block within its braces ({}) is executed by only one thread at a time.
        // 2. _lockObj is an object used for synchronization. It serves as a mutual exclusion lock (mutex).
        // 3. When a thread reaches this statement, it attempts to acquire the lock on _lockObj.
        // 4. If another thread has already acquired the lock, the current thread will wait (or block) until the lock is released.
    }
}

Step 2. Implementation of “CustomLogger.cs”.

using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace LockUnlockExample
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            string logFilePath = @"D:\SystemLogger\ApplicationLog.txt"; // Substitute your file path with this one.
            CustomLogger customLogger = new CustomLogger(logFilePath);

            // Establish and commence several threads to imitate concurrent logging.
            Thread[] threads = new Thread[5];
            for (int i = 0; i < threads.Length; i++)
            {
                int threadIndex = i;
                threads[i] = new Thread(() =>
                {
                    for (int j = 0; j < 10; j++)
                    {
                        customLogger.LogToFile($"Thread {threadIndex} - Log entry {j}");
                        Thread.Sleep(new Random().Next(100)); // Simulate work
                    }
                });
                threads[i].Start();
            }
            // Ensure that all threads have finished before proceeding.
            foreach (Thread thread in threads)
            {
                thread.Join();
            }
            Console.WriteLine("Logging has been completed by all threads.");
        }
    }
}

Explanation of the above code

Step 3. The diagram below illustrates the process of how the locking and unlocking mechanism works

  1. Threads: Numerous threads are attempting to write log messages.
  2. Lock Object: The _lockObj object utilized for synchronization.
  3. Critical Section: The code enclosed within the lock block, is responsible for writing to the log file.
    Critical section

Steps of thread locking and unlocking mechanism of the above diagram

  1. Threads Attempt to Acquire Lock: All threads (Thread1, Thread2, Thread3, etc.) try to acquire the lock on the _lockObj. - Only one thread can hold the lock at a time.
  2. Thread1 Acquires Lock: Thread1 successfully acquires the lock and enters the critical section. - The critical section contains the code responsible for writing to the log file.
  3. Thread1 Writes to Log: Inside the critical section, Thread1 creates a StreamWriter and writes a log message to the file. - Once done writing, the StreamWriter is properly disposed of.
  4. Thread1 Releases Lock: Thread1 exits the critical section, releasing the lock on _lockObj. - This allows other waiting threads (Thread2, Thread3) to acquire the lock.
  5. Thread2 Acquires Lock: Thread2 acquires the lock and enters the critical section. - Thread2 writes to the log file in a similar manner as Thread1.
  6. Thread2 Releases Lock: Thread2 releases the lock, allowing the next thread (Thread3) to acquire it.
  7. Thread3 Acquires Lock: Thread3 acquires the lock, enters the critical section, and writes to the log file.
  8. Threads Continue Execution: After each thread writes to the log file and releases the lock, they continue with their execution.

By following this sequence, each thread can write to the log file in a thread-safe manner, avoiding any interleaving of log entries and ensuring data integrity.