In C#, the lock
keyword and Monitor
class can be used to address the issues of multi-threaded resource locking and deadlocks.
Official Explanation: The
lock
statement acquires a mutual lock on a given object, executes a code block, and then releases the lock.
Next, we will explore the use of the lock
keyword and the Monitor
class.
1, Lock
Lock
is used to lock a reference type, allowing only one thread to access this object at any given time. Lock
is syntactic sugar that is realized through Monitor
.
The object locked by Lock
should be a static reference type (except for strings).
In fact, strings can also be used as locking objects, but due to the uniqueness of string objects, it may cause conflicts between different threads in different locations. If you can ensure the uniqueness of the string, such as a string generated by Guid
, it can be used as a locking object (though it is not recommended). The locking object does not necessarily have to be static; it can also be an instance variable of a class used as a lock object.
Lock Prototype
Lock
is syntactic sugar for Monitor
, the generated code comparison is as follows:
lock (x)
{
// Your code...
}
object __lockObj = x;
bool __lockWasTaken = false;
try
{
System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
// Your code...
}
finally
{
if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}
Here we will not discuss Monitor
, we will cover it later.
Lock Code Example
First, if you write like the example below, you might as well go out and hit si
:
public void MyLock()
{
object o = new object();
lock (o)
{
//
}
}
Now let’s write a simple lock, as shown below:
class Program
{
private static object obj = new object();
private static int sum = 0;
static void Main(string[] args)
{
Thread thread1 = new Thread(Sum1);
thread1.Start();
Thread thread2 = new Thread(Sum2);
thread2.Start();
while (true)
{
Console.WriteLine($"{DateTime.Now.ToString()}:" + sum);
Thread.Sleep(TimeSpan.FromSeconds(1));
}
}
public static void Sum1()
{
sum = 0;
lock (obj)
{
for (int i = 0; i < 10; i++)
{
sum += i;
Console.WriteLine("Sum1");
Thread.Sleep(TimeSpan.FromSeconds(2));
}
}
}
public static void Sum2()
{
sum = 0;
lock (obj)
{
for (int i = 0; i < 10; i++)
{
sum += 1;
Console.WriteLine("Sum2");
Thread.Sleep(TimeSpan.FromSeconds(2));
}
}
}
}
The class sets itself as the lock, which can prevent malicious code from locking a public object.
For example:
public void Access()
{
lock(this) {}
}
Locking can prevent other threads from executing the code in the lock block (lock(o){}
); when locked, other threads must wait for the locking thread to finish executing and release the lock. However, this may have a performance impact on the program. Locking is not very suitable for I/O scenarios, such as file I/O; complex computations or long-running operations can cause significant performance degradation.
10 ways to optimize lock performance: http://www.thinkingparallel.com/2007/07/31/10-ways-to-reduce-lock-contention-in-threaded-programs/
2, Monitor
This object provides a mechanism for synchronizing access to objects; Monitor
is a static type with fewer methods, with commonly used methods as follows:
| Operation | Description |
|-----------|-------------|
| Enter, TryEnter | Acquires a lock on the object. This operation also marks the beginning of a critical section. No other thread can enter the critical section unless it uses a different locking object to execute within the section. |
| Wait | Releases the lock on the object to allow other threads to lock and access the object. The calling thread will wait for another thread to access the object. A pulse signal is used to notify the waiting thread about changes in the object's state. |
| Pulse, PulseAll | Sends a signal to one or more waiting threads. The signal notifies waiting threads that the state of the locked object has changed, and the lock owner is ready to release the lock. Waiting threads are placed in the ready queue of the object, so they may eventually receive the lock of the object. Once a thread gets locked, it can check the new state of the object to see if it has reached the desired state. |
| Exit | Releases the lock on the object. This operation also marks the end of the critical section that is protected by the locked object. |
How to Use
Here is a very simple example:
private static object obj = new object();
private static bool acquiredLock = false;
public static void Test()
{
try
{
Monitor.Enter(obj, ref acquiredLock);
}
catch { }
finally
{
if (acquiredLock)
Monitor.Exit(obj);
}
}
Monitor.Enter
locks the object obj
and sets acquiredLock
to true, indicating that obj
is locked.
Finally, at the end, check acquiredLock
, release the lock, and set acquiredLock
to false.
Explanation
Critical Section: Refers to the area surrounded by certain symbols. For example, within {}
.
The Enter
and Exit
methods of the Monitor
object mark the beginning and end of the critical section.
The Enter()
method guarantees that only a single thread can use the code in the critical section after acquiring the lock. When using the Monitor
class, it is best to pair it with try{...}catch{...}finally{...}
because if the lock is acquired but not released, it can cause other threads to be blocked indefinitely, resulting in a deadlock.
Generally, the lock
keyword is sufficient.
Example
The following demonstrates how multiple threads can use Monitor
to implement locks:
private static object obj = new object();
private static bool acquiredLock = false;
static void Main(string[] args)
{
new Thread(Test1).Start();
Thread.Sleep(1000);
new Thread(Test2).Start();
}
public static void Test1()
{
try
{
Monitor.Enter(obj, ref acquiredLock);
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Test1 is locking the resource");
Thread.Sleep(1000);
}
}
catch { }
finally
{
if (acquiredLock)
Monitor.Exit(obj);
Console.WriteLine("Test1 has released the resource");
}
}
public static void Test2()
{
bool isGetLock = false;
Monitor.Enter(obj);
try
{
Monitor.Enter(obj, ref acquiredLock);
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Test2 is locking the resource");
Thread.Sleep(1000);
}
}
catch { }
finally
{
if (acquiredLock)
Monitor.Exit(obj);
Console.WriteLine("Test2 has released the resource");
}
}
Setting Lock Timeout
If the object is already locked, another thread attempting to use Monitor.Enter
on the object will wait indefinitely until the lock is released.
However, if a thread encounters a problem or if there is a deadlock situation, the lock remains locked. Or if a thread has a timeout and is not executed for a period, it becomes meaningless.
We can set a waiting time using Monitor.TryEnter()
. If the lock is not released after a certain time, it will return false.
Here’s how to modify the previous example:
private static object obj = new object();
private static bool acquiredLock = false;
static void Main(string[] args)
{
new Thread(Test1).Start();
Thread.Sleep(1000);
new Thread(Test2).Start();
}
public static void Test1()
{
try
{
Monitor.Enter(obj, ref acquiredLock);
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Test1 is locking the resource");
Thread.Sleep(1000);
}
}
catch { }
finally
{
if (acquiredLock)
Monitor.Exit(obj);
Console.WriteLine("Test1 has released the resource");
}
}
public static void Test2()
{
bool isGetLock = false;
isGetLock = Monitor.TryEnter(obj, 500);
if (!isGetLock)
{
Console.WriteLine("The lock has not been released, I won't work");
return;
}
try
{
Monitor.Enter(obj, ref acquiredLock);
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Test2 is locking the resource");
Thread.Sleep(1000);
}
}
catch { }
finally
{
if (acquiredLock)
Monitor.Exit(obj);
Console.WriteLine("Test2 has released the resource");
}
}
There are many advanced and complex techniques for using locks; this article simply introduces the use of Lock
and Monitor
.
As the tutorial progresses, we will continue to learn many advanced usage methods.
文章评论