When performing parallel processing (multi-threading), if multiple threads try to modify a single variable or resource at the same time, a “race condition” can occur. This can destroy data integrity.
In C#, the lock statement is the most basic way to restrict a block of code so that only one thread can execute it at a time (exclusive control).
This article explains an implementation example of calculating a maximum value in a thread-safe manner using PLINQ.
Implementing Exclusive Control with the lock Statement
The following sample code simulates processing a list of server operation data in parallel. It calculates the average response time for each server while identifying the overall “slowest response time (maximum value).”
Since the process of updating the maximum value might be accessed by multiple threads simultaneously, we protect it using lock.
Sample Code
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
// 1. Data Source: Access statistics for each server
var serverLogs = new List<ServerMetric>
{
new ServerMetric { ServerId = "SV-001", RequestCount = 500, TotalDurationMs = 25000 },
new ServerMetric { ServerId = "SV-002", RequestCount = 450, TotalDurationMs = 13500 }, // Fast
new ServerMetric { ServerId = "SV-003", RequestCount = 600, TotalDurationMs = 42000 }, // Slow
new ServerMetric { ServerId = "SV-004", RequestCount = 300, TotalDurationMs = 15000 },
new ServerMetric { ServerId = "SV-005", RequestCount = 550, TotalDurationMs = 22000 },
};
// Instance of the analysis class
var analyzer = new LogAnalyzer();
Console.WriteLine("Starting parallel analysis...");
// 2. Parallel processing with PLINQ
// Use ForAll to execute the analysis method for each element
serverLogs.AsParallel()
.ForAll(log => analyzer.CalculateAndCompare(log));
// Output results
Console.WriteLine("\n--- Individual Server Results ---");
foreach (var log in serverLogs)
{
Console.WriteLine($"[{log.ServerId}] Avg Duration: {log.AverageDuration:F2} ms");
}
Console.WriteLine("\n--- Overall Statistics ---");
Console.WriteLine($"Max Avg Time (Worst): {analyzer.MaxAverageTime:F2} ms");
}
}
// Server statistics data class
public class ServerMetric
{
public string ServerId { get; set; }
public int RequestCount { get; set; } // Total requests
public double TotalDurationMs { get; set; } // Total duration
public double AverageDuration { get; set; } // Average (stored after calculation)
}
// Analysis logic class
public class LogAnalyzer
{
// Lock object for exclusive control
// Using a private readonly reference type is standard practice
private readonly object _syncRoot = new object();
// Shared resource: Maximum value
public double MaxAverageTime { get; private set; } = 0.0;
public void CalculateAndCompare(ServerMetric metric)
{
// 1. Local calculation
// This part is per-instance and does not conflict with other threads, so no lock is needed
if (metric.RequestCount > 0)
{
metric.AverageDuration = metric.TotalDurationMs / metric.RequestCount;
}
// 2. Critical Section (Accessing shared resources)
// Lock this because reading and updating MaxAverageTime simultaneously leads to incorrect values
lock (_syncRoot)
{
if (metric.AverageDuration > MaxAverageTime)
{
MaxAverageTime = metric.AverageDuration;
}
}
}
}
Explanation and Important Points
Selecting the Lock Object
It is best practice to use a dedicated private readonly object variable for the lock. If you lock on this, a Type object, or a publicly accessible string, there is a risk of lock contention (such as deadlocks) occurring in unintended places.
Minimizing the Critical Section
While code inside the lock { ... } block (critical section) is running, other threads must wait.
As shown in the sample code, it is important to keep local calculations (like calculating individual averages) outside the lock. Only lock the minimum necessary parts (like updating shared variables) to maintain parallel processing performance.
Memory Barrier and Visibility
The lock statement performs exclusive control and also acts as a memory barrier. This ensures that variable values updated by one thread are visible as the latest state to other threads.
