- Mastering C# Concurrency
- Eugene Agafonov Andrew Koryavchenko
- 1496字
- 2021-07-09 21:26:05
Using locks
There are different types of locks in C# and .NET. We will cover these later in the chapter, and also throughout the book. Let us start with the most common way to use a lock in C#, which is a lock
statement.
Lock statement
Lock statement in C# uses a single argument, which could be an instance of any class. This instance will represent the lock itself.
Reading other people's codes, you could see that a lock uses the instance of collection or class, which contains shared data. It is not a good practice, because someone else could use this object for locking, and potentially create a deadlock situation. So, it is recommended to use a special private synchronization object, the sole purpose of which is to serve as a concrete lock:
// Bad lock(myCollection) { myCollection.Add(data); } // Good lock(myCollectionLock) { myCollection.Add(data); }`
Note
It is dangerous to use lock(this)
and lock(typeof(MyType))
. The basic idea why it is bad remains the same: the objects you are locking could be publicly accessible, and thus someone else could acquire a lock on it causing a deadlock. However, using the this
keyword makes the situation more implicit; if someone else made the object public, it would be very hard to track that it is being used inside a lock.
Locking the type object is even worse. In the current versions of .NET, the runtime type objects could be shared across application domains (running in the same process). It is possible because those objects are immutable. However, this means that a deadlock could be caused, not only by another thread, but also by ANOTHER APPLICATION, and I bet that you would hardly understand what's going on in such a case.
Following is how we can rewrite the first example with race condition and fix it using C# lock statement. Now the code will be as follows:
const int iterations = 10000; var counter = 0; var lockFlag = new object(); ThreadStart proc = () => { for (int i = 0; i < iterations; i++) { lock (lockFlag) counter++; Thread.SpinWait(100); lock (lockFlag) counter--; } }; var threads = Enumerable .Range(0, 8) .Select(n => new Thread(proc)) .ToArray(); foreach (var thread in threads) thread.Start(); foreach (var thread in threads) thread.Join(); Console.WriteLine(counter);
Now this code works properly, and the result is always 0.
To understand what is happening when a lock statement is used in the program, let us look at the Intermediate Language code, which is a result of compiling C# program. Consider the following C# code:
static void Main() { var ctr = 0; var lockFlag = new object(); lock (lockFlag) ctr++; }
The preceding block of code will be compiled into the following:
.method private hidebysig static void Main() cil managed { .entrypoint // Code size 48 (0x30) .maxstack 2 .locals init ([0] int32 ctr, [1] object lockFlag, [2] bool '<>s__LockTaken0', [3] object CS$2$0000, [4] bool CS$4$0001) IL_0000: nop IL_0001: ldc.i4.0 IL_0002: stloc.0 IL_0003: newobj instance void [mscorlib]System.Object::.ctor() IL_0008: stloc.1 IL_0009: ldc.i4.0 IL_000a: stloc.2 .try { IL_000b: ldloc.1 IL_000c: dup IL_000d: stloc.3 IL_000e: ldloca.s '<>s__LockTaken0' IL_0010: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&) IL_0015: nop IL_0016: ldloc.0 IL_0017: ldc.i4.1 IL_0018: add IL_0019: stloc.0 IL_001a: leave.s IL_002e } // end .try finally { IL_001c: ldloc.2 IL_001d: ldc.i4.0 IL_001e: ceq IL_0020: stloc.s CS$4$0001 IL_0022: ldloc.s CS$4$0001 IL_0024: brtrue.s IL_002d IL_0026: ldloc.3 IL_0027: call void [mscorlib]System.Threading.Monitor::Exit(object) IL_002c: nop IL_002d: endfinally } // end handler IL_002e: nop IL_002f: ret } // end of method Program::Main
This can be explained with decompilation to C#. It will look like this:
static void Main() { var ctr = 0; var lockFlag = new object(); bool lockTaken = false; try { System.Threading.Monitor.Enter(lockFlag, ref lockTaken); ctr++; } finally { if (lockTaken) System.Threading.Monitor.Exit(lockFlag); } }
It turns out that the lock statement turns into calling the Monitor.Enter
and Monitor.Exit
methods, wrapped into a try
-finally
block. The Enter
method acquires an exclusive lock and returns a bool value, indicating that a lock was successfully acquired. If something went wrong, for example an exception has been thrown, the bool value would be set to false
, and the Exit
method would release the acquired lock.
A try
-finally
block ensures that the acquired lock will be released even if an exception occurs inside the lock statement. If the Enter
method indicates that we cannot acquire a lock, then the Exit
method will not be executed.
Monitor class
The Monitor
class contains other useful methods that help us to write concurrent code. One of such methods is the TryEnter
method, which allows the provision of a timeout value to it. If a lock could not be obtained before the timeout is expired, the TryEnter
method would return false
. This is quite an efficient method to prevent deadlocks, but you have to write significantly more code.
Consider the previous deadlock sample refactored in a way that one of the threads uses Monitor.TryEnter
instead of lock
:
static void Main() { const int count = 10000; var a = new object(); var b = new object(); var thread1 = new Thread( () => { for (int i = 0; i < count; i++) lock (a) lock (b) Thread.SpinWait(100); }); var thread2 = new Thread(() => LockTimeout(a, b, count)); thread1.Start(); thread2.Start(); thread1.Join(); thread2.Join(); Console.WriteLine("Done"); } static void LockTimeout(object a, object b, int count) { bool accquiredB = false; bool accquiredA = false; const int waitSeconds = 5; const int retryCount = 3; for (int i = 0; i < count; i++) { int retries = 0; while (retries < retryCount) { try { accquiredB = Monitor.TryEnter(b, TimeSpan.FromSeconds(waitSeconds)); if (accquiredB) { try { accquiredA = Monitor.TryEnter(a, TimeSpan.FromSeconds(waitSeconds)); if (accquiredA) { Thread.SpinWait(100); break; } else { retries++; } } finally { if (accquiredA) { Monitor.Exit(a); } } } else { retries++; } } finally { if (accquiredB) Monitor.Exit(b); } } if (retries >= retryCount) Console.WriteLine("could not obtain locks"); } }
In the LockTimeout
method, we implemented a retry strategy. For each loop iteration, we try to acquire lock B
first, and if we cannot do so in 5 seconds, we try again. If we have successfully acquired lock B
, then we in turn try to acquire lock A
, and if we wait for it for more than 5 seconds, we try again to acquire both the locks. This guarantees that if someone waits endlessly to acquire a lock on B
, then this operation will eventually succeed.
If we do not succeed acquiring lock B
, then we try again for a defined number of attempts. Then either we succeed, or we admit that we cannot obtain the needed locks and go to the next iteration.
In addition, the Monitor
class can be used to orchestrate multiple threads into a workflow with the Wait
, Pulse
, and PulseAll
methods. When a main thread calls the Wait
method, the current lock is released, and the thread is blocked until some other thread calls the Pulse
or PulseAll
methods. This allows the coordination the different threads execution into some sort of sequence.
A simple example of such workflow is when we have two threads: the main thread and an additional thread that performs some calculation. We would like to pause the main thread until the second thread finishes its work, and then get back to the main thread, and in turn block this additional thread until we have other data to calculate. This can be illustrated by the following code:
var arg = 0; var result = ""; var counter = 0; var lockHandle = new object(); var calcThread = new Thread(() => { while (true) lock (lockHandle) { counter++; result = arg.ToString(); Monitor.Pulse(lockHandle); Monitor.Wait(lockHandle); } }) { IsBackground = true }; lock (lockHandle) { calcThread.Start(); Thread.Sleep(100); Console.WriteLine("counter = {0}, result = {1}", counter, result); arg = 123; Monitor.Pulse(lockHandle); Monitor.Wait(lockHandle); Console.WriteLine("counter = {0}, result = {1}", counter, result); arg = 321; Monitor.Pulse(lockHandle); Monitor.Wait(lockHandle); Console.WriteLine("counter = {0}, result = {1}", counter, result); }
As a result of running this program, we will get the following output:
counter = 0, result = counter = 1, result = 123 counter = 2, result = 321
At first, we start a calculation thread. Then we print the initial values for counter
and result
, and then we call Pulse
. This puts the calculation thread into a queue called ready queue. This means that this thread is ready to acquire this lock as soon as it gets released. Then we call the Wait
method, which releases the lock and puts the main thread into a waiting queue. The first thread in the ready queue, which is our calculation thread, acquires the lock and starts to work. After completing its calculations, the second thread calls Pulse
, which moves a thread at the head of the waiting queue (which is our main thread) into the ready queue. If there are several threads in the waiting queue, only the first one would go into the ready queue. To put all the threads into the ready queue at once, we could use the PulseAll
method. So, when the second thread calls Wait
, our main thread reacquires the lock, changes the calculation data, and repeats the whole process one more time.
Note
Note that we can use the Wait
, Pulse
, and PulseAll
methods only when the current thread owns a lock. The Wait
method could block indefinitely in case no other threads call Pulse
or PulseAll
, so it can be a reason for a deadlock. To prevent deadlocks, we can specify a timeout value to the Wait
method to be able to react in case we cannot reacquire the lock for a certain time period.
- Interactive Data Visualization with Python
- HTML5 移動Web開發(fā)從入門到精通(微課精編版)
- Data Analysis with Stata
- C程序設(shè)計案例教程
- SQL Server與JSP動態(tài)網(wǎng)站開發(fā)
- Express Web Application Development
- PLC應(yīng)用技術(shù)(三菱FX2N系列)
- Zabbix Performance Tuning
- 軟件體系結(jié)構(gòu)
- Java7程序設(shè)計入門經(jīng)典
- Hacking Android
- C# 7.0本質(zhì)論
- Spark for Data Science
- Python數(shù)據(jù)分析與挖掘?qū)崙?zhàn)(第2版)
- Learning Yeoman