Benefits of Locking and Unlocking Objects in C#

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

  • The lock statement is employed to obtain a lock on a specific object before entering a critical section.
  • Only one thread can possess the lock at any given time, preventing other threads from accessing the locked code section until the lock is relinquished.

(b) Releasing an Object

  • The lock is automatically released when the thread exits the code block associated with the lock statement. This guarantees that the lock is always released, even if an exception is thrown within the locked code block.
  • Consider a practical real-life situation where numerous threads attempt to record messages to a file. It is crucial to prioritize thread safety in this scenario to prevent any mixing or corruption of log entries.

Real Scenario

  • Imagine you have an application that processes various tasks in parallel, and each task needs to log its progress to a shared log file. To ensure that the log entries are written correctly without being interleaved, you need to use a locking mechanism.
  • In this instance, we will establish a CustomLogger class that offers a method to log messages to a file in a thread-safe manner. The lock statement will be utilized to guarantee that only a single thread can write to the log file at any given time.

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

  • Threads are initiated and activated to mimic simultaneous logging operations.
  • Each thread invokes the Log method of the Logger object, providing its thread index and a log message.
  • The Thread. Sleep function within each thread imitates a pause in activity before the subsequent log entry.

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.


Similar Articles