Multi-threading is used in almost all real-life applications. I summed up my thoughts on use of locks and deadlock prevention in the following related topics:
From a thread safety perspective, resources (memory) is classified as either thread-exclusive, read-only, or lock-protected.
Every object is a monitor, basically, you create a private readonly object and lock on it. Only one thread will be allowed in the critical section at a time:
1: Monitor.Enter(_lock);
2:
3: try
4: {
5: DoWork();
6: }
7: finally
8: {
9: Monitor.Exit(_lock);
10: }
Or use the shorthand:
1: lock (_lock)
2: {
3: DoWork();
4: }
Can be used to ensure that a job is done only once; first thread to enter is the "victim" that does the job; others wait:
1: private void Initialize()
3: if (_isInitialized)
5: return;
7:
8: lock (_lock)
9: {
10: if (!_isInitialized)
11: {
12: DoWork();
13: _isInitialized = true;
14: }
15: }
16: }
17:
18: private bool _isInitialized = false;
Locking is essential during lazy initialization:
1: public HeavyObject LazyInitialized
3: if (_data == null)
5: lock (_lock)
6: {
7: if (_data == null)
9: _data = new HeavyObject();
11: }
12: }
13:
14: return _data;
16:
17: private HeavyObject _data = null;
1: // This is WRONG! It returns a reference to the shared resource.
2: // After the mutex, the client code can change the content of the object
3: public Example SharedResource
5: get
7: lock (_lock)
9: return _data;
14: // Deep-copy the data first and return the copy
15: // Returns a snapshot of the shared data at a certain point in time
16: public Example SharedResource
17: {
18: get
19: {
20: lock (_lock)
21: {
22: new Example(_data); // Copy constructor that performs a deep copy
23: }
24: }
25: }
1: public class Example
3: public void DecrementFoo(int delta)
7: _foo -= delta;
8: }
9: }
10:
11: public void IncrementBar(int delta)
12: {
13: lock (_lock)
14: {
15: _bar += delta;
17: }
18:
19: public void DecrementFooAndIncrementBar(int delta)
20: {
21: lock (_lock)
22: {
23: // Lock is acquired; these calls will not be blocked
24: DecrementFoo(delta);
25: IncrementBar(delta);
26: }
27: }
28:
29: private int _foo = 0;
30: private int _bar = 0;
31: private readonly object _lock = new object();
32: }
Access operations are classified as either reads or writes
ReaderWriterLock
ReaderWriterLockSlim
Supported in
.Net 1.0+
.Net 3.5+
Usage
Robustness: thread aborts and OOM exceptions
Performance: 3x to 6x faster
Reentrance
Supported but not advised
Supported (LockRecursionPolicy) but not advised
1: public void WriteAll()
3: Debug.Assert(!_lock.IsReadLockHeld && !_lock.IsWriteLockHeld,
4: "Lock is held by others.");
5:
6: _lock.EnterWriteLock();
8: try
10: WriteX();
11: WriteY();
13: finally
15: _lock.ExitWriteLock();
19: public void WriteX()
21: // This assertion will catch this defect
22: Debug.Assert(!_lock.IsReadLockHeld && !_lock.IsWriteLockHeld,
23: "Lock is held by others.");
24:
25: // This will cause the deadlock
26: // Waiting for the lock, held by WriteAll(), to be relinquished
27: _lock.EnterWriteLock();
29: try
30: {
31: _x = _a + _b;
33: finally
34: {
35: _lock.ExitWriteLock();
36: }
37: }
One possible solution is to set a timeout using TryEnterWriteLock(timeout) and TryEnterReadLock(timeout), but it's not recommended because this doesn't guarantee mutual exclusion
The fix is to re-factor the actual work done by WriteX() into a private helper method and make WriteAll() and WriteX() call it:
10: DoWriteX();
11: DoWriteY();
21: Debug.Assert(!_lock.IsReadLockHeld && !_lock.IsWriteLockHeld,
22: "Lock is held by others.");
23:
24: _lock.EnterWriteLock(); // No problem :)
25:
26: try
27: {
28: DoWriteX();
29: }
30: finally
31: {
32: _lock.ExitWriteLock();
33: }
34: }
35:
36: public void DoWriteX()
37: {
38: Debug.Assert(_lock.IsWriteLockHeld,
39: "The required write lock is NOT held by this thread.");
40:
41: _x = _a + _b;
42: }
Here's an example of locking using ReaderWriterLockSlim that also shows the atomicity of variable references and use of the volatile modifier:
3: public int Foo
5: // Atomic;
6: // Lock is not required to read a 32-bit volatile value
7: get { return _foo; }
9:
10: public int Bar
12: get
13: {
14: Debug.Assert(!_lock.IsWriteLockHeld,
15: "Another thread is holding the write lock.");
17: _lock.EnterReadLock();
19: try
21: // Atomic; but _bar is NOT volatile; read lock is needed
22: return _bar; // Value-type, no need to return a copy (auto-boxed)
24: finally
25: {
26: _lock.ExitReadLock();
28: }
30:
31: public void DecrementFoo(int delta)
32: {
33: Debug.Assert(!_lock.IsReadLockHeld && !_lock.IsWriteLockHeld,
34: "Lock is held by others.");
36: _lock.EnterWriteLock();
37:
38: try
39: {
40: DoDecrementFoo(delta);
41: }
42: finally
43: {
44: _lock.ExitWriteLock();
45: }
46: }
47:
48: public void IncrementBar(int delta)
49: {
50: Debug.Assert(!_lock.IsReadLockHeld && !_lock.IsWriteLockHeld,
51: "Lock is held by others.");
52:
53: _lock.EnterWriteLock();
54:
55: try
56: {
57: DoIncrementBar(delta);
58: }
59: finally
60: {
61: _lock.ExitWriteLock();
62: }
63: }
64:
65: private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
66: private volatile int _foo = 0;
67: private int _bar = 0;
68: }
See also: http://msdn.microsoft.com/en-us/library/sbhbke0y.aspx
We already know that some operations are guaranteed to be atomic (like reading a 32-bit value).
.Net has the Interlocked class which provides some common functionality that can be called in an atomic manner. Consider the following example:
1: public void IncrementFooBy1()
3: lock (_lock)
5: _foo++;
7: }
The mutex block _foo++; is compiled into 3 assembly instructions that look similar to the following:
1: MOV EAX, [_foo] // Load
2: INC EAX // Increment
3: MOV [_foo], EAX // Save
The instructions above are not guaranteed to be atomic. However, the same functionality can be accomplished using the following code instead:
3: Interlocked.Increment(ref _foo);
In this case, the CLR guarantees that it's an atomic operation, which looks similar to the following assembly instruction:
1: LOCK INC DWORD PTR [_foo]
See also: my post on debugging deadlocks.
I know that it’s a long read, but I hope it was worth it. I’d like to thank Vance Morrison and Philip Kelley for sharing their knowledge about this topic.