CodeGym /Courses /C# SELF /Best practices and tools for diagnostics

Best practices and tools for diagnostics

C# SELF
Level 57 , Lesson 4
Available

1. The secret to resilient multithreaded code

If you imagine multithreading as a team of several workers fixing a car at the same time, it's clear: as soon as one of them grabs someone else's tool — the repair stalls. In code it's the same: careless handling of shared data leads to subtle bugs that may only show up "in production".

In this lecture — how to write multithreaded code that doesn't collapse like a house of cards. Also: which tools help when something does go wrong.

1. Minimize critical sections (lock)

The less code inside a lock, the better. While one thread holds the lock, others wait.

Example:


// BAD: All business logic inside lock — all threads wait
lock(_locker)
{
    // Long operation (not related to shared resource)
    Thread.Sleep(500);
    counter++;
}

// GOOD: Only the minimal necessary action inside lock
// heavy work is outside the lock
Thread.Sleep(500);
lock(_locker)
{
    counter++;
}

Real life: If a network call or a long calculation ends up inside the lock, performance drops sharply.

2. Don’t use universal objects as lock keys

Writing lock(this) or lock(typeof(MyClass)) is a bad idea.

Why? If someone else uses the same object for their lock, you'll get deadlocks or hidden bugs. Always use a dedicated private object:

private readonly object _locker = new object();

lock(_locker)
{
    // Your actions
}

Forbidden: strings (string), public fields, value-type objects.

3. Always use try...finally to release acquired resources

Any acquisition of Mutex, a semaphore, ReaderWriterLockSlim — must be released in finally.

_mutex.WaitOne();
try
{
    // Critical section
}
finally
{
    _mutex.ReleaseMutex();
}

4. Don’t overuse synchronization

Synchronize only access to real shared resources (for example, collections), not every little thing. Excessive locks turn code into a waiting queue.

5. Use thread-safe collections and types

.NET provides special collections for multithreaded scenarios: ConcurrentDictionary, ConcurrentQueue, ConcurrentBag, BlockingCollection and others. They protect themselves internally.

using System.Collections.Concurrent;

ConcurrentDictionary<int, string> users = new ConcurrentDictionary<int, string>();
users.TryAdd(1, "Sean");
users[2] = "Bob";

6. Beware of deadlock

A common trap is acquiring multiple locks in different orders.

// Thread 1
lock(obj1)
{
    lock(obj2)
    {
        // Do something
    }
}

// Thread 2
lock(obj2)
{
    lock(obj1)
    {
        // Do something
    }
}

Tip: Always acquire locks in the same order across all threads.

7. Prefer immutable state when possible

If an object doesn't change state after creation — it's safe to read from any thread. Examples: string, Tuple, DateTime, your own read-only DTOs.

2. Tools for diagnosing multithreading issues

Synchronization bugs are sneaky: they appear rarely and unpredictably. Use tools and approaches that help catch and analyze them.

1. Event and thread logging

Log current Thread.ManagedThreadId and key operations — it's a simple way to understand "who and when" entered/exited a critical section.

Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Entered critical section");
// ...
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Exited critical section");

For real apps use Microsoft.Extensions.Logging, NLog, Serilog.

2. Thread Sanitizer & Race Detector

There isn't a perfect built-in ThreadSanitizer in .NET, but there are useful tools:

3. Visual Studio Diagnostics Tools

Visual Studio profilers help you see:

  • which threads exist in the app;
  • where threads are idle (waiting);
  • where locks and contention occur;
  • when deadlock and contention appear.

Capture traces to get a detailed graph of lock usage.

4. Dump analysis and WinDbg

If the server "hung", take a process dump and open it in WinDbg or dotnet-dump. From call stacks you can see where threads are stuck and who holds which locks.

Example of stack analysis:

0:000> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
    1 000001d4b6f90e08          1         1 000001d4b5c941c0 000001d4b6f03458

(Dumps are usually used by "deployment jedis" — don't be afraid, it's a powerful tool.)

5. Unit testing with stress (stress testing)

Run multithreaded code in parallel for hundreds/thousands of iterations — rare races are easier to find that way.

[Test]
public void Counter_IsThreadSafe()
{
    var counter = 0;
    var locker = new object();
    var tasks = new List<Task>();
    for (int i = 0; i < 100; i++)
    {
        tasks.Add(Task.Run(() =>
        {
            for (int j = 0; j < 10000; j++)
            {
                lock (locker)
                {
                    counter++;
                }
            }
        }));
    }
    Task.WaitAll(tasks.ToArray());
    Assert.AreEqual(100 * 10000, counter);
}

6. Use asserts and special checks

Add checks that guarantee correct state in debug. For example, Debug.Assert when a resource is attempted to be re-acquired by the same thread.

3. Conclusions and recommendations

Visual diagram: danger zone and safety

graph TD
    A[Shared resource] -- without synchronization --> B(Race condition)
    A -- lock (lock/Mutex) --> C[Safe access: critical section]
    C -- "too many locks" --> D(Performance loss)
    A -- ReaderWriterLockSlim --> E{Many readers / One writer}
    E -- "Read" --> F[Many threads read concurrently]
    E -- "Write" --> G[Only one writes, others wait]

Synchronization primitives and their purpose

Primitive What it's for How many threads it allows Cross-process Performance Where to use
lock (Monitor)
Simple critical section 1 No Very high 99% of cases
Mutex
The same section, but between processes 1 Yes Medium Files, IPC
Semaphore
No more than N threads N Yes Medium Resource pools
SemaphoreSlim
Same, but faster, within a process N No High Pools in code
ReaderWriterLockSlim
Many readers, one writer Many/1 No High Caches, settings
2
Task
C# SELF, level 57, lesson 4
Locked
Eliminating Livelock with Random Pauses
Eliminating Livelock with Random Pauses
1
Survey/quiz
Deadlocks, level 57, lesson 4
Unavailable
Deadlocks
Typical Multithreading Issues
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION