Vs. Net multi-thread control statement mutex
1. Monitor. Enter and monitor. Exit
The monitor class grants an object lock to a single thread to control access to the object. Object locks provide the ability to restrict access to code blocks (usually known as critical sections. When a thread has an object lock, no other thread can obtain the lock. You can also use monitor to ensure that no other thread is allowed to access the application code section being executed by the lock owner, unless another thread is using another lock object to execute the code.
Note: Use monitor to lock the object (that is, the reference type) instead of the value type.
Monitor has the following functions:
It is associated with an object as needed.
It is not bound, that is, it can be called directly from any context.
You cannot create a monitor instance.
The following information is maintained for each synchronization object:
Reference to the thread currently holding the lock.
References to the ready queue, which contains the thread for preparing to obtain the lock.
A reference to the waiting queue. It contains a thread waiting for notification of status changes of the locked object.
Use the enter and exit methods to mark the beginning and end of a critical section.
If the critical section is a continuous instruction set, the lock obtained by the enter method ensures that only one thread can use the Lock Object to execute the Code contained. In this case, we recommend that you put these commands in the try block and put the exit command in the Finally block. This function is usually used to synchronize access to static classes or instance methods.
If the instance method requires synchronous thread access, it will use the current instance as the object to be locked to call the enter and corresponding exit methods. Since only one thread can hold the lock on the current instance, this method can only be executed by one thread at a time.
The static method uses the type of the current instance as the Lock Object to protect it in a similar way. The enter and exit methods provide the same functions as the C # Lock statements.
If the critical section spans the entire method, you can place system. runtime. compilerservices. methodimplattried on the method and specify the synchronized value in the methodimplattried constructor to implement the above locking function. After using this attribute, you do not need the enter and exit statements. Note that this attribute will hold the lock on the current thread until the method is returned. If the lock can be released earlier, use the monitor class or C # Lock statement instead of this attribute.
Although the enter and exit statements for locking and releasing a given object can span the boundaries of members or classes or both, This is not recommended.
When selecting the object to be synchronized, only private or internal objects should be locked. Locking external objects may lead to deadlocks because unrelated code may choose to lock the same object for different purposes.
// Instance method synchronous thread access
Public class account
{
Int val;
Public void deposit (int x)
{
Monitor. Enter (this );
Try
{
Val + = X;
}
Finally
{
Monitor. Exit (this );
}
}
Public void withdraw (int x)
{
Monitor. exiter (this );
Try
{
Val-= X;
}
Finally
{
Monitor. Exit (this );
}
}
}
// Static method synchronous thread access
Public class demostatic
{
Public static int COUNT = 0;
}
Public class demostaticlock
{
Public static void demo ()
{
Try
{
Monitor. Enter (typeof (demostatic ));
Demostatic. Count ++;
}
Catch (exception E)
{
Console. writeline ("Capture exceptions {0}", E. tostring ());
}
Finally
{
Monitor. Exit (typeof (demostatic ));
}
}
}
2. Lock/synclock statements
The lock/synclock keyword marks a statement as a critical section.
// Instance method synchronous thread access
Public class account
{
Int val;
Public void deposit (int x)
{
Lock (this)
{
Val + = X;
}
}
Public void withdraw (int x)
{
Lock (this)
{
Val-= X;
}
}
}
3. readerwriterlock
Readerwriterlock is used to synchronize access to resources. At any specific time, it allows multiple threads to simultaneously read or write data to a single thread. When resources do not change frequently, readerwriterlock provides a higher throughput than simply allowing only one thread lock (such as monitor) at a time.
Readerwriterlock has the best performance when most access requests are read requests and the Write Access frequency is low and the duration is short. Multiple read threads and a single write thread are operated in turn, so neither the read thread nor the write thread will be blocked for a long time.
Note: holding the read or write thread lock for a long time will cause other threads to become hungry (starve ). To achieve the best performance, you need to consider re-constructing the application to minimize the write access duration.
A thread can hold a read or write thread lock, but cannot hold both. To obtain the write thread lock, use upgradetowriterlock and downgradefromwriterlock instead of releasing the read thread lock.
Recursive lock requests increase the lock count.
The read and write threads are respectively added to their respective queues. When the thread releases the write thread lock, all the waiting threads in the read thread queue will be granted the read thread lock. When all the read thread locks have been released, the next thread in the write thread queue in the waiting state (if any) will be granted the write thread lock, and so on. In other words, readerwriterlock operates alternately between a set of read threads and a write thread.
When a thread in the write thread queue is waiting for the active read thread lock to be released, the thread requesting the new read thread lock will be discharged into the read thread queue. Even if they can be accessed together with the existing read thread lock holders, their requests are not granted; this can prevent the write thread from being blocked indefinitely by the read thread.
Most methods to obtain a lock on readerwriterlock use a timeout value. Timeout can avoid deadlocks in applications. For example, a thread may obtain the write thread lock on a resource and then request the read thread lock on the second resource. At the same time, the other thread obtains the write thread lock on the second resource and requests the read thread lock on the first resource. If no timeout is used, the two threads will experience a deadlock.
If the timeout interval expires and no lock request is granted, this method throws applicationexception to control the return to the call thread. The thread can capture this exception and determine the operation to be performed next.
Timeout is expressed in milliseconds. If system. timespan is used to specify the timeout value, the value used is the sum of the millisecond integers expressed by timespan.
The following shows the valid timeout value in milliseconds.
Value description
-1 infinite.
0: No timeout.
> 0: the number of milliseconds to wait.
A negative timeout value is not allowed except-1. If you want to use a negative integer other than-1 to specify the timeout, the system uses zero (no timeout ). If the specified timespan is a negative millisecond value other than-1, argumentoutofrangeexception is thrown.
Use readerwriterlock for mutex: First instantiate readerwriterlock, and then call the acquirereaderlock method before reading the critical resource. After the read process ends, call releasereaderlock to release the read lock. before modifying the critical resource, call the acquirewriterlock method to request a write lock. After the write process ends, call the releasewriterlock method to release the write lock.
// Example
Public class account
{
Int val;
Readerwriterlock RWL = new readerwriterlock ();
Public int read ()
{
RWL. acquirereaderlock (timeout. Infinite );
Int iret = 0;
Try
{
Iret = val;
}
Finally
{
RWL. releasereaderlock ();
}
Return iret;
}
Public void deposit (int x)
{
RWL. acquirewriterlock (timeout. Infinite );
Try
{
Val + = X;
}
Finally
{
RWL. releasewriterlock ();
}
}
}
4. mutex
When two or more threads need to access a shared resource at the same time, the system needs to use a synchronization mechanism to ensure that only one thread uses the resource at a time. Mutex is a synchronization primitive that grants only one thread the exclusive access to shared resources. If a thread obtains the mutex, the second thread to obtain the mutex will be suspended until the first thread releases the mutex.
You can use waithandle. waitone to request the ownership of the mutex. Threads with mutex can request the same mutex in repeated wait calls without blocking its execution. However, the thread must call the releasemutex method for the same number of times to release the ownership of the mutex. If a thread terminates normally when it has a mutex, the state of the mutex is set to terminate, And the next waiting thread obtains the ownership. If no thread has a mutex, the state of the mutex is terminated.
Public class account
{
Int val = 100;
Mutex M = new mutex ();
Public void deposite (int x)
{
M. waitone (); // request to obtain the mutex object
Try
{
Val + = X;
}
Finally
{
M. releasemutex (); // release the mutex object
}
}
Public void withdraw (int x)
{
M. waitone ();
Try
{
Val-= X;
}
Finally
{
M. releasemutex ();
}
}
}
Mutex objects can be used to synchronize across processes between threads. Although mutex does not have all the wait and Pulse Functions of the monitor class, it does provide the ability to create mutex that can be used between processes. The following code:
Using system;
Using system. Threading;
Namespace demosyncacrossproc
{
Public class app
{
Static public void demomutex ()
{
Mutex = new mutex (false, "Demo ");
Console. writeline ("Create a mutex named Demo ");
If (mutex. waitone ())
Console. writeline ("Get mutex ");
Else
Console. writeline ("No mutex is obtained ");
Console. writeline ("press any key to exit ");
Console. Readline ();
}
Static int main ()
{
Demomutex ();
Return 0;
}
}
}
After compilation, open a command window and run the program without pressing any key. The program is shown as follows:
Create a mutex named demo
Obtain the mutex.
Press any key to exit
Open the 2nd Command window and execute the program. The program is displayed as follows:
Create a mutex named demo
At this time, 2nd instances are blocked (because the 1st Application Instances have not released the mutex named demo ).
Switch to the 1st command line window, press any key to end the 1st application instances, and then switch to the 2nd Command Line window. Then, the program continues to execute and output "Get mutex ".
5. Interlocked
This method can prevent errors that may occur in the following situations: when a thread is updating a variable that can be accessed by another thread, the scheduler switches the context; or two threads can be executed simultaneously on different processors. Such members do not cause exceptions.
The increment and decrement Methods increase or decrease variables and store the result values in a single operation. On most computers, adding a variable is not an atomic operation. You need to perform the following steps:
(1). Load the values in instance variables into registers.
(2). increase or decrease the value.
(3). Store the value in the instance variable.
If increment and decrement are not used, the thread will be preemptible after the first two steps are completed. Then, the other thread executes all three steps. When the first thread starts execution again, It overrides the value in the instance variable, resulting in loss of the result of the increase or decrease operation in the second thread.
The exchange method automatically exchanges the value of a specified variable. The compareexchange method combines two operations: compare two values and store the third value in one of the variables based on the comparison results. Compare and exchange operations are performed by atomic operations.
Exchange and compareexchange methods exposed by interlocked use parameters of the object type that can be referenced. However, type security requires that all parameters be strictly typed as objects. An object cannot be forcibly converted to an object in a method call. In other words, an object type variable must be created, assign the custom object to the variable and then pass the variable. For example:
Public class demointerlocked
{
Object _ x;
Public object x
{
Set
{
Object ovalue = value;
Interlocked. compareexchange (ref _ x, ovalue, null );
}
Get
{
Return _ x;
}
}
}
// The following example uses interlocked to implement mutual exclusion
Using system;
Using system. Threading;
Namespace demosyncresource
{
Class Resource
{
Readerwriterlock RWL = new readerwriterlock ();
Public void read int32 (threadnum)
{
RWL. acquirereaderlock (timeout. Infinite );
Try
{
Console. writeline ("starting to read resources (thread = {0})", threadnum );
Thread. Sleep (250 );
Console. writeline ("End of resource reading (thread = {0})", threadnum );
}
Finally
{
RWL. releasereaderlock ();
}
}
Public void write (int32 threadnum)
{
RWL. acquirewriterlock (timeout. Infinite );
Try
{
Console. writeline ("start to write resources (thread = {0})", threadnum );
Thread. Sleep (750 );
Console. writeline ("End of write resource (thread = {0})", threadnum );
}
Finally
{
RWL. releasewriterlock ();
}
}
}
Class app
{
// Critical resource
Static int32 numasyncops = 4;
// Synchronization object
Static autoresetevent asyncaredone = new autoresetevent (false );
// Critical resource
Static resource res = new resource ();
Public static void main ()
{
For (int32 threadnum = 0; threadnum <4; threadnum ++)
{
Threadpool. queueuserworkitem (New waitcallback (updateresource), threadnum );
}
Asyncaredone. waitone ();
Console. writeline ("all operations are completed ");
}
Static void updateresource (object state)
{
Int32 threadnum = (int32) State;
If (threadnum % 2 )! = 0)
Res. Read (threadnum );
Else
Res. Write (threadnum );
// Use interlocked. decrement to mutually exclusive modification of critical resources
// Reduce numasyncops by 1 for every thread executed
// If numasyncops changes to 0, all four threads are finished.
If (interlocked. decrement (ref numasyncops) = 0)
Asyncaredone. Set ();
}
}
}
Comparison of these five mutex implementation methods:
A. Monitor. Enter/monitor. Exit and lock (OBJ)/synclock are both based on the reference object cumbersome technology. Locks are bundled with critical resources. The two methods are coarse-grained.
B. mutex is based on its own lock. By associating a critical resource with a mutex instance, all threads requesting the critical resource obtain the mutex lock related to it first. The locking granularity of this method can be controlled freely, and can be an object, a piece of code, or even the whole process.
C. interlocked provides the finest granularity-based lock. It does not depend on the lock, but is based on the atomicity of atomic operations, it makes increment, subtraction, exchange, comparison, and other actions an inseparable atomic operation to achieve mutual exclusion.