With the introduction of multithreading, you may feel that you need to understand some issues related to thread sharing resources. NET Framework provides many classes and data types to control access to shared resources.
Consider a situation we often encounter: There are some global variables and shared class variables, We need to update them from different threads, you can use the system. threading. the interlocked class completes such a task and provides atomic, non-modular integer update operations.
In addition, you can use the system. Threading. Monitor class to lock a piece of code of the object method so that it cannot be accessed by other threads temporarily.
An instance of the system. Threading. waithandle class can be used to encapsulate OS-specific objects waiting for exclusive access to shared resources. Especially for the interoperability of unmanaged code.
System. Threading. mutex is used to synchronize multiple complex threads. It also allows single-threaded access.
Synchronization event classes such as manualresetevent and autoresetevent support a thread that notifies other events.
If we do not discuss thread synchronization, we do not know much about multi-threaded programming, but we should be very careful when using multi-threaded synchronization. When using thread synchronization, we must be able to correctly determine in advance that the object and method may cause a deadlock (deadlock means that all threads have stopped the corresponding, are waiting for the other party to release resources ). There is also the issue of dirty data (which refers to the inconsistency caused by operations on data by multiple threads at the same time). This is not easy to understand. Let's just say, there are two threads X and Y. Thread X reads data from the file and writes data to the data structure. Thread y reads data from the data structure and sends the data to other computers. Assuming that X writes data while y reads data, the data read by Y is obviously inconsistent with the data actually stored. We should avoid this situation. A small number of threads will make the problem happen less often, and the access to shared resources will be better synchronized.
The. NET Framework CLR provides three methods to complete shared resources, such as global variable domains, specific code segments, static and instantiated methods and domains.
(1) code domain synchronization: The monitor class can be used to synchronize all or part of code segments of static/instantiated methods. Static domain synchronization is not supported. In the instantiation method, this pointer is used for synchronization, while in static methods, classes are used for synchronization, which will be discussed later.
(2) manual synchronization: use different synchronization classes (such as waithandle, mutex, readerwriterlock, manualresetevent, autoresetevent, and interlocked) to create their own synchronization mechanisms. This synchronization method requires you to manually synchronize different domains and methods. This synchronization method can also be used for inter-process synchronization and deadlock relief caused by waiting for shared resources.
(3) Context synchronization: Use synchronizationattribute to create simple and automatic synchronization for the contextboundobject object. This synchronization method is only used for the synchronization of Instantiation methods and domains. All objects in the same context domain share the same lock.
Monitor class
Only one thread can access the specified code segment at a given time. The monitor class is very suitable for Thread Synchronization in this case. The methods in this class are static, so you do not need to instantiate this class. The following static methods provide a mechanism for synchronizing Object Access to avoid deadlocks and maintain data consistency.
Monitor. Enter method: Obtain the exclusive lock on the specified object.
Monitor. tryenter method: attempts to obtain the exclusive lock of the specified object.
Monitor. Exit method: Release the exclusive lock on the specified object.
Monitor. Wait method: Release the lock on the object and block the current thread until it gets the lock again.
Monitor. Pulse Method: notifies the thread in the waiting queue of changing the status of the locked object.
Monitor. pulseall method: notifies all the pending thread object state changes.
You can synchronize the access to the code segment by locking and unlocking the specified object. Monitor. Enter, monitor. tryenter, and monitor. Exit are used to lock and unlock a specified object. Once the lock of the specified object (code segment) is obtained (the monitor. Enter is called), other threads cannot obtain the lock. For example, thread x obtains an object lock, which can be released (Call monitor. Exit (object) or monitor. Wait ). When this object lock is released, the monitor. Pulse method and the monitor. pulseall method notify the next thread of the ready queue to perform the lock and all other threads in the ready queue to obtain the exclusive lock. Thread x releases the lock and thread y obtains the lock. Thread X that calls monitor. Wait enters the waiting queue. When the thread (thread y) of the currently locked object is affected by pulse or pulseall, the thread waiting for the queue enters the ready queue. When thread X Re-obtains the object lock, monitor. Wait returns. If the thread y that owns the lock does not call pulse or pulseall, the method may be locked uncertain. Pulse, pulseall and wait must be the synchronized code segment called. For each synchronization object, you need a pointer to the thread with the current lock, a ready queue, and a waiting queue (including the thread that needs to be notified of the status change of the locked object.
You may ask, what happens when two threads call monitor. Enter at the same time? No matter how close the two threads call monitor. Enter, there must actually be one before, one after, so there will always be only one to get the object lock. Since monitor. Enter is an atomic operation, the CPU cannot be biased towards one thread rather than another. To achieve better performance, you should delay the call to obtain the lock from the next thread and immediately release the object lock from the previous thread. Locking is feasible for private and internal objects, but may lead to deadlocks for external objects, because unrelated code may lock the same object for different purposes.
If you want to lock a piece of code, it is best to add the lock setting statement in the try statement and put monitor. exit in the finally statement. To lock the entire code segment, you can use the methodimplattribute (in the system. runtime. compilerservices namespace) class to set the synchronization value in its constructor. This is an alternative method. When the locking method returns, the lock is released. If you need to release the lock quickly, you can use the Statement of the monitor class and C # Lock to replace the above method.
Let's look at a piece of code that uses the monitor class:
Public void some_method ()
{
Int A = 100;
Int B = 0;
Monitor. Enter (this );
// Say we do something here.
Int c = A/B;
Monitor. Exit (this );
}
The code above may cause problems. When the code runs to int c = A/B;, an exception is thrown, and monitor. Exit will not return. Therefore, this program will be suspended and other threads will not be locked. There are two ways to solve the problem above. The first method is to put the code into try... In finally, when finally calls monitor. Exit, the lock will be released. The second method is to use the lock () method of C. Calling this method has the same effect as calling monitoy. Enter. However, once the code execution exceeds the permitted range, the release lock will not automatically occur. See the following code:
Public void some_method ()
{
Int A = 100;
Int B = 0;
Lock (this );
// Say we do something here.
Int c = A/B;
}
The C # Lock statement provides the same functionality as monitoy. Enter and monitoy. Exit. This method is used when your code segment cannot be interrupted by other independent threads.
Waithandle class
The waithandle class is used as the base class, which allows multiple waiting operations. This class encapsulates the Win32 synchronous processing method. The waithandle object notifies other threads that they need exclusive access to resources. Other threads must wait until waithandle no longer uses resources and the waiting handle is not used. The following are several classes inherited from it:
Mutex: A synchronization primitive can also be used for inter-process synchronization.
Autoresetevent: notifies one or more waiting threads that an event has occurred. This class cannot be inherited.
Manualresetevent: when one or more pending thread events are notified. This class cannot be inherited.
These classes define some signal mechanisms to possess and release exclusive access to resources. They have two statuses: signaled and nonsignaled. The waiting handle in the signaled state does not belong to any thread unless it is in the nonsignaled state. The set method is no longer used by threads that have a waiting handle. Other threads can call the reset method to change the State or any waithandle method requires a waiting handle. The methods are as follows:
Waitall: waits for all elements in the specified array to receive signals.
Waitany: waits for any element in the specified array to receive a signal.
Waitone: when being rewritten in a derived class, the current thread is blocked until the current waithandle receives the signal.
These wait Methods block threads until one or more synchronization objects receive signals.
The waithandle object encapsulates the operating system-specific objects waiting for exclusive access to the shared resources. Both the managed code and the unmanaged code can be used. However, it is not easy to use monitor. Monitor is fully managed code and is very efficient for operating system resources.
Mutex class
Mutex is another method for synchronizing between threads and across processes. It also provides synchronization between processes. It allows a thread to exclusively share resources while blocking access from other threads and processes. The mutex name demonstrates the exclusive possession of resources by its owner. Once a thread has mutex, all other threads that want to get mutex will be suspended until the occupying thread releases it. The mutex. releasemutex method is used to release mutex. A thread can call the wait method multiple times to request the same mutex, but the same number of mutex. releasemutex must be called when the mutex is released. If no thread occupies mutex, the mutex state changes to signaled; otherwise, it is nosignaled. Once the mutex status changes to signaled, wait for the next thread in the queue to get the mutex. The mutex class corresponds to the createmutex of Win32. The method for creating a mutex object is very simple. The following methods are commonly used:
A thread can obtain mutex ownership by calling waithandle. waitone, waithandle. waitany, or waithandle. waitall. If mutex does not belong to any thread, the above call will make the thread have mutex, and waitone will return immediately. However, if other threads have mutex, waitone will wait for an indefinite period until mutex is obtained. You can specify a parameter in the waitone method, that is, the waiting time, to avoid waiting for mutex indefinitely. Calling close to act on mutex will release the ownership. Once mutex is created, you can use the gethandle method to obtain the mutex handle and use it for the waithandle. waitany or waithandle. waitall methods.
The following is an example:
Public void some_method ()
{
Int A = 100;
Int B = 20;
Mutex firstmutex = new mutex (false );
Firstmutex. waitone ();
// Some kind of processing can be done here.
Int x = A/B;
Firstmutex. Close ();
}
In the preceding example, the thread creates a mutex, but does not declare its ownership at the beginning. by calling the waitone method, the thread owns the mutex.
Synchronization events
The synchronization time is the waiting handle used to notify other threads of what happened and the resources are available. They have two statuses: signaled and nonsignaled. Autoresetevent and manualresetevent are synchronization events.
Autoresetevent class
This class can notify one or more threads of an event. When a waiting thread is released, it converts the status to signaled. Use the set method to change its instance status to signaled. However, once the notification time of the waiting thread changes to signaled, its turntable will automatically change to nonsignaled. If no thread listens for the event, the turntable will remain signaled. This class cannot be inherited.
Manualresetevent class
This class is also used to notify one or more thread events. Its status can be set and reset manually. The manual reset time will remain in the signaled state until manualresetevent. Reset sets the status to nonsignaled or nonsignaled until manualresetevent. Set sets the status to signaled. This class cannot be inherited.
Interlocked class
It provides synchronization of shared variable access between threads. Its operations are atomic and shared by threads. you can use interlocked. increment or interlocked. decrement to increase or decrease shared variables. it is a bit like an atomic operation. That is to say, these methods can generate an integer parameter increment and return a new value. All operations are one step. you can also use it to specify the value of a variable or check whether the two variables are equal. If they are equal, the specified value will replace the value of one of the variables.
Readerwriterlock class
It defines a lock that provides a unique write/Multi-read mechanism for read/write synchronization. any number of threads can read data. Data locks are required when data is updated by threads. the read thread can obtain the lock only when there is no written thread. when there is no read thread or other write threads, the write thread can get the lock. therefore, once writer-lock is requested, all read threads cannot read data until the write thread completes access. it supports pausing to avoid deadlocks. it also supports nested read/write locks. the nested read lock is supported by readerwriterlock. acquirereaderlock: If a thread has a write lock, the thread will be suspended;
The nested write lock is supported by readerwriterlock. acquirewriterlock. If a thread has a read lock, the thread is suspended. if there is a read lock, it will easily be a deadlock. the safe method is to use readerwriterlock. upgradetowriterlock method, which will upgrade the reader to the writer. you can use readerwriterlock. the downgradefromwriterlock method degrades the writer to a reader. call readerwriterlock. releaselock will release the lock, readerwriterlock. restorelock will reload the lock status to call readerwriterlock. before releaselock.
Conclusion:
This section describes.. NET platform thread synchronization issues. in the next series of articles, I will give some examples to further illustrate these methods and techniques. although the use of thread synchronization will bring great value to our program, we 'd better be careful with these methods. otherwise, the benefits will not be brought, but performance will decline or even program crashes. only a large number of contacts and experiences can you master these skills. use as few things as possible that cannot be completed or are uncertain blocked in synchronous code blocks, especially I/O operations; use local variables instead of global variables as much as possible; synchronization is used in places where some code is accessed by multiple threads and different states are shared by different processes; arranging your code to precisely control each data in a thread; code that is not shared among threads is secure. In the next article, we will learn about thread pools.