Contents

Concerning shared resources

Possible issues in multi-threaded environments

The program doesn’t produce a result – it crashes or freezes. The program gives an incorrect result. The program produces a correct result but doesn’t satisfy some non-function-related requirement – it spends too much time or resources.

Deadlock

Race-Condition

Busy-Wait

while(!hasSomethingHappened) ;

Thread Starvation

Synchronization methods

Interlocked

public static int CompareExchange (ref int location1, int value, int comparand);

var original = location1; if (location1 == comparand) location1 = value; return original;

class MyClass { public event EventHandler MyEvent; }

[CompilerGenerated] private EventHandler MyEvent; public event EventHandler MyEvent { [CompilerGenerated] add { EventHandler eventHandler = this.MyEvent; EventHandler comparand; do { comparand = eventHandler; eventHandler = Interlocked.CompareExchange<EventHandler>(ref this.MyEvent, (EventHandler) Delegate.Combine((Delegate) comparand, (Delegate) value), comparand); } while (eventHandler != comparand); } [CompilerGenerated] remove { // The same algorithm but with Delegate.Remove } }

EventHandler eventHandler = this.MyEvent; EventHandler comparand; do { comparand = eventHandler; // Begin Atomic Operation if (MyEvent == comparand) { eventHandler = MyEvent; MyEvent = Delegate.Combine(MyEvent, value); } // End Atomic Operation } while (eventHandler != comparand);

Monitor.Enter, Monitor.Exit, lock

lock(a) { lock (a) { ... } }

var syncObject = new Object(); Monitor.Enter(syncObject); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); await Task.Delay(1000); Monitor.Exit(syncObject); Console.WriteLine(Thread.CurrentThread.ManagedThreadId);

while(!TryEnter(syncObject)) ;

SpinLock, SpinWait

Monitor.Wait, Monitor.Pulse[All]

object syncObject = new object(); Thread t1 = new Thread(T1); t1.Start(); Thread.Sleep(100); Thread t2 = new Thread(T2); t2.Start();

ReaderWriterLockSlim

var @lock = new ReaderWriterLockSlim(); @lock.EnterReadLock(); try { // ... } finally { @lock.ExitReadLock(); }

class RWLock : IDisposable { public struct WriteLockToken : IDisposable { private readonly ReaderWriterLockSlim @lock; public WriteLockToken(ReaderWriterLockSlim @lock) { this.@lock = @lock; @lock.EnterWriteLock(); } public void Dispose() => @lock.ExitWriteLock(); } public struct ReadLockToken : IDisposable { private readonly ReaderWriterLockSlim @lock; public ReadLockToken(ReaderWriterLockSlim @lock) { this.@lock = @lock; @lock.EnterReadLock(); } public void Dispose() => @lock.ExitReadLock(); } private readonly ReaderWriterLockSlim @lock = new ReaderWriterLockSlim(); public ReadLockToken ReadLock() => new ReadLockToken(@lock); public WriteLockToken WriteLock() => new WriteLockToken(@lock); public void Dispose() => @lock.Dispose(); }

var rwLock = new RWLock(); // ... using(rwLock.ReadLock()) { // ... }

The ResetEvent family

AutoResetEvent evt = new AutoResetEvent(false); Thread t1 = new Thread(T1); t1.Start(); Thread.Sleep(100); Thread t2 = new Thread(T2); t2.Start();

Conclusions

When working with threads, there are two issues that may lead to incorrect results or even the absence of results – race condition and deadlock.

Issues that can make the program spend more time or resources are thread starvation and busy wait.

.NET provides a lot of ways to synchronize threads.

There are two modes of block waits – Spin Wait and Core Wait. Som.e thread synchronization primitives in .NET use both of them.

Interlocked is a set of atomic operations which can be used to implement lock-free algorithms. It’s the fastest synchronization primitive.

The lock and Monitor.Enter/Exit operators implement the concept of a critical section – a code fragment that can only be executed by one thread at one point of time.

The Monitor.Pulse/Wait methods are useful for implementing Producer-Consumer scenarios.

ReaderWriterLockSlim can be more useful than the standard locking cases when parallel reading is expected.

The ResetEvent class family can be useful for thread synchronization.