As you deepen your learning of multi-threading, you may feel that you need to understand some issues about thread-shared resources. .NET framework provides a lot of 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 that we need to update from different threads, and such tasks can be accomplished by using classes, which provide atomic, non-modular integer update operations.
There is also a piece of code that can use a class to lock objects, so that it cannot be accessed by other threads for the time being.
An instance of a class can be used to encapsulate an operating system-specific object waiting for exclusive access to a shared resource. Especially for interoperability issues with unmanaged code.
For the problem of synchronization of multiple complex threads, it also allows single thread access.
Synchronous event classes like ManualResetEvent and AutoResetEvent support a class that notifies threads of other events.
Not discussing thread synchronization issues means that we know very little about multi-threaded programming, but we must be very cautious about using multi-threaded synchronization. When using thread synchronization, we must be able to correctly determine in advance which object and method may cause deadlock (deadlock means that all threads stop corresponding and are waiting for the other party to release resources). There is also the problem of stolen data (referring to the inconsistency caused by multiple threads operating on the data at the same time), which is not easy to understand. Let's put it this way, there are two threads X and Y. Thread X reads data from the file and writes data to the data structure, and thread Y reads data from this data structure and sends the data to other computers. Assuming that while Y reads the data and X writes the data, then it is obvious that the data read by Y is inconsistent with the actual stored data. This situation is obviously something we should avoid. A small number of threads will make the problem just now have much less chance of occurring, and access to shared resources will be better synchronized.
.NET Framework's CLR provides three methods to complete the sharing of shared resources, such as global variable domains, specific code segments, static and instantiated methods and domains.
(1) Code domain synchronization: Use the Monitor class to synchronize all code or part of the code segment of the static/instified method. Synchronization of static domains is not supported. In the instantiated method, this pointer is used for synchronization; in the static method, the class is used for synchronization, which will be discussed later.
(2) Manual synchronization: Use different synchronization classes (such as WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent, AutoResetEvent and Interlocked, etc.) to create your own synchronization mechanism. This synchronization method requires you to manually synchronize different domains and methods. This synchronization method can also be used for synchronization between processes and the release of deadlock caused by waiting for shared resources.
(3) Context synchronization: Use SynchronizationAttribute to create simple, automatic synchronization for the ContextBoundObject object. This synchronization method is only used for synchronization of instantiated methods and domains. All objects in the same context domain share the same lock.
Monitor Class
At a given time and specified code segments, the Monitor class is very suitable for thread synchronization in this case. The methods in this class are all static, so there is no need to instantiate this class. The following static methods provide a mechanism to synchronize access to objects to avoid deadlocks and maintain data consistency.
Method: Obtain an exclusive lock on the specified object.
Method: Try to obtain the exclusive lock of the specified object.
Method: Release the exclusive lock on the specified object.
Method: Release the lock on the object and block the current thread until it reacquires the lock.
Method: Notify the thread in the waiting queue to change the state of the locked object.
Method: Notify all waiting thread object state changes.
Access to code segments can be synchronized by locking and unlocking the specified object. , and used to lock and unlock the specified object. Once the lock of the specified object (code segment) is obtained (invoked), no other thread can acquire the lock. For example, thread X obtains an object lock, which can be released (object) or ). When this object lock is released, the method and method notify the next thread of the ready queue and all other ready queue threads will have a chance to acquire the exclusive lock. Thread X releases the lock and thread Y acquires the lock, and the callees enters the waiting queue. When the thread (thread Y) that is currently locked from the currently locked object is subjected to Pulse or PulseAll, the thread waiting for the queue enters the ready queue. Thread X only returns when the object lock is regained. If the thread that has the lock (thread Y) does not call Pulse or PulseAll, the method may be locked indeterminately. Pulse, PulseAll and Wait must be synchronized code segments called. For each synchronized object, you need a pointer to the thread that currently has the lock, a ready queue and a pointer to the waiting queue (including threads that need to be notified of the state change of the locked object).
You might ask, what happens when two threads call at the same time? No matter how close the two threads call are, there must actually be one in front and one in the back, so there will always be only one to obtain the object lock. Since it is an atomic operation, it is impossible for the CPU to prefer one thread instead of another. For better performance, you should delay the acquisition lock call of the latter thread and immediately release the object lock of the previous thread. Locking is feasible for private and internal objects, but for external objects it may cause deadlocks because irrelevant code may lock the same object for different purposes.
If you want to lock a piece of code, the best thing is to add a statement that sets the lock in the try statement and put it in the finally statement. For locking of the entire snippet, you can set the synchronous value in its constructor using the MethodImplAttribute class. This is an alternative method, and when the locking method returns, the lock is released. If you need to release the lock quickly, you can use the Monitor class and C# lock declaration instead of the above method.
Let's look at a piece of code using the Monitor class:
public void some_method()
{
int a=100;
int b=0;
(this);
//say we do something here.
int c=a/b;
(this);
}
Running the above code will cause problems. When the code runs to int c=a/b; , an exception will be thrown and will not be returned. Therefore, this program will be suspended and other threads will not get the lock. There are two ways to solve the above problem. The first method is: put the code into try... finally and call it finally, so that the lock will be released in the end. The second method is: use the lock() method of C#. Calling this method has the same effect as calling it. However, once the code execution is out of range, releasing the lock will not occur automatically. See the code below:
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 and this method is used in situations where your code segment cannot be interrupted by other independent threads.
WaitHandle Class
The WaitHandle class is used as a base class, which allows multiple wait operations. This class encapsulates the synchronization processing method of win32. The WaitHandle object notifies other threads that it requires exclusive access to the resource, and other threads must wait until the WaitHandle no longer uses the resource and the wait handle is not used. Here are several classes inherited from it:
Mutex class: Synchronous primitives can also be used for inter-process synchronization.
AutoResetEvent: Notifies one or more threads waiting for an event that has occurred. This kind cannot be inherited.
ManualResetEvent: Appears when one or more waiting thread events have occurred. This kind cannot be inherited.
These classes define some signaling mechanisms to possess and release exclusive access to resources. They have two states: signed and nonsignaled. The waiting handle for the Signaled state does not belong to any thread unless it is a nonsignaled state. Threads that have waiting handles no longer use the waiting handles to use the set method. Other threads can call the Reset method to change the state or any WaitHandle method requires that they have a waiting handle. These methods are shown below:
WaitAll: Wait for all elements in the specified array to receive the signal.
WaitAny: Wait for any element in the specified array to receive the signal.
WaitOne: When overridden in the derived class, block the current thread until the current WaitHandle receives the signal.
These wait methods block the thread until one or more synchronous objects receive a signal.
The WaitHandle object encapsulates operating system-specific objects waiting for exclusive access to shared resources, whether they are managed or unmanaged code. But it is not as lightweight as Monitor, which is completely managed code and is very efficient in using operating system resources.
Mutex Class
Mutex is another method to complete inter-thread and cross-process synchronization, and it also provides inter-process synchronization. It allows one thread to exclusively occupy shared resources while blocking access to other threads and processes. The name of Mutex is a good example of its owner's exclusive possession of resources. Once a thread owns Mutex, the other threads that want Mutex will hang until the occupying thread releases it. Methods are used to release Mutex. A thread can call the wait method multiple times to request the same Mutex, but the same number of times must be called when releasing Mutex. If no thread owns Mutex, then the state of Mutex becomes signed, otherwise it is nosignaled. Once the Mutex state becomes signed, the next thread waiting for the queue will get the Mutex. The Mutex class corresponds to the CreateMutex of win32. The method of creating Mutex objects is very simple. The following methods are commonly used:
A thread can obtain ownership of Mutex by calling or or . If Mutex does not belong to any thread, the above call will make the thread own Mutex, and WaitOne will return immediately. But if there are other threads that have Mutex, WaitOne will fall into an indefinite wait until Mutex is obtained. You can specify parameters in the WaitOne method, i.e. the waiting time, to avoid waiting for Mutex indefinitely. Calling Close to act on Mutex will release possession. Once Mutex is created, you can obtain the Mutex handle through the GetHandle method and use it for the or method.
Here is an example:
public void some_method()
{
int a=100;
int b=20;
Mutex firstMutex = new Mutex(false);
();
//some kind of processing can be done here.
Int x=a/b;
();
}
In the example above, the thread creates a Mutex, but it does not declare that it has it at the beginning, and owns Mutex by calling the WaitOne method.
Synchronization Events
Synchronization time is some waiting handles used to notify other threads about what happened and resources are available. They have two states: signed and nonsignaled. AutoResetEvent and ManualResetEvent are such synchronization events.
AutoResetEvent Class
This class can notify one or more threads of events. When a waiting thread is released, it converts the state to signaled. Use the set method to change its instance state to signaled. However, once the waiting thread is notified to be signed, its turntable will automatically become nonsignaled. If no threads listen for events, the turntable will remain signed. This type cannot be inherited.
ManualResetEvent Class
This class is also used to notify one or more thread events that have occurred. Its status can be manually set and reset. Manual reset time will remain signed until its state is set to nonsignaled, or keep it to nonsignaled until its state is set to signaled. This class cannot be inherited.
Interlocked Class
It provides synchronization of shared variable access between threads. Its operations are atomic operations and are shared by threads. You can increase or decrease shared variables by or. Its point is that it is atomic operations, that is, these methods can increment the parameter of an integer and return a new value. All operations are one step. You can also use it to specify the value of the variable or check whether the two variables are equal. If it is equal, the specified value will be replaced by the value of one of the variables.
ReaderWriterLock class
It defines a lock, providing a unique write/multi-read mechanism, so that read and write synchronization. Any number of threads can read data, and the data lock will be required when there are threads to update data. The read thread can acquire the lock, if and only if there is no thread written here. When there is no read thread and other write threads, the write thread can get the lock. Therefore, once the writer-lock is requested, all read threads will not be able to read the data until the write thread is accessed. It supports pause and avoids deadlocks. It also supports nested read/write locks. The method to support nested read locks is that if a thread has a write lock, the thread will be paused;
The method to support nested write locks is that if a thread has a read lock, the thread will pause. If there is a read lock, it will easily be deadlocked. The safe way is to use the method, which will upgrade the reader to the writer. You can use the method to downgrade the writer to the reader. The call will release the lock and reload the state of the lock until it is called.
in conclusion:
This part talks about the problem of thread synchronization on the .NET platform. 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 programs, we should be better off using these methods carefully. Otherwise, it will not bring benefits, but will lead to performance degradation or even program crashes. Only a large number of connections and experiences can enable you to master these techniques. Try to use things that cannot be completed or are uncertainly blocked in the synchronized code block, especially I/O operations; use local variables as much as possible instead of global variables; synchronization is used where part of the code is accessed by multiple threads and processes and the state is shared by different processes; arrange your code so that each data is accurately controlled in one thread; it is safe to not share the code between threads; in the next article, we will learn about thread pool knowledge.
Consider a situation we often encounter: there are some global variables and shared class variables that we need to update from different threads, and such tasks can be accomplished by using classes, which provide atomic, non-modular integer update operations.
There is also a piece of code that can use a class to lock objects, so that it cannot be accessed by other threads for the time being.
An instance of a class can be used to encapsulate an operating system-specific object waiting for exclusive access to a shared resource. Especially for interoperability issues with unmanaged code.
For the problem of synchronization of multiple complex threads, it also allows single thread access.
Synchronous event classes like ManualResetEvent and AutoResetEvent support a class that notifies threads of other events.
Not discussing thread synchronization issues means that we know very little about multi-threaded programming, but we must be very cautious about using multi-threaded synchronization. When using thread synchronization, we must be able to correctly determine in advance which object and method may cause deadlock (deadlock means that all threads stop corresponding and are waiting for the other party to release resources). There is also the problem of stolen data (referring to the inconsistency caused by multiple threads operating on the data at the same time), which is not easy to understand. Let's put it this way, there are two threads X and Y. Thread X reads data from the file and writes data to the data structure, and thread Y reads data from this data structure and sends the data to other computers. Assuming that while Y reads the data and X writes the data, then it is obvious that the data read by Y is inconsistent with the actual stored data. This situation is obviously something we should avoid. A small number of threads will make the problem just now have much less chance of occurring, and access to shared resources will be better synchronized.
.NET Framework's CLR provides three methods to complete the sharing of shared resources, such as global variable domains, specific code segments, static and instantiated methods and domains.
(1) Code domain synchronization: Use the Monitor class to synchronize all code or part of the code segment of the static/instified method. Synchronization of static domains is not supported. In the instantiated method, this pointer is used for synchronization; in the static method, the class is used for synchronization, which will be discussed later.
(2) Manual synchronization: Use different synchronization classes (such as WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent, AutoResetEvent and Interlocked, etc.) to create your own synchronization mechanism. This synchronization method requires you to manually synchronize different domains and methods. This synchronization method can also be used for synchronization between processes and the release of deadlock caused by waiting for shared resources.
(3) Context synchronization: Use SynchronizationAttribute to create simple, automatic synchronization for the ContextBoundObject object. This synchronization method is only used for synchronization of instantiated methods and domains. All objects in the same context domain share the same lock.
Monitor Class
At a given time and specified code segments, the Monitor class is very suitable for thread synchronization in this case. The methods in this class are all static, so there is no need to instantiate this class. The following static methods provide a mechanism to synchronize access to objects to avoid deadlocks and maintain data consistency.
Method: Obtain an exclusive lock on the specified object.
Method: Try to obtain the exclusive lock of the specified object.
Method: Release the exclusive lock on the specified object.
Method: Release the lock on the object and block the current thread until it reacquires the lock.
Method: Notify the thread in the waiting queue to change the state of the locked object.
Method: Notify all waiting thread object state changes.
Access to code segments can be synchronized by locking and unlocking the specified object. , and used to lock and unlock the specified object. Once the lock of the specified object (code segment) is obtained (invoked), no other thread can acquire the lock. For example, thread X obtains an object lock, which can be released (object) or ). When this object lock is released, the method and method notify the next thread of the ready queue and all other ready queue threads will have a chance to acquire the exclusive lock. Thread X releases the lock and thread Y acquires the lock, and the callees enters the waiting queue. When the thread (thread Y) that is currently locked from the currently locked object is subjected to Pulse or PulseAll, the thread waiting for the queue enters the ready queue. Thread X only returns when the object lock is regained. If the thread that has the lock (thread Y) does not call Pulse or PulseAll, the method may be locked indeterminately. Pulse, PulseAll and Wait must be synchronized code segments called. For each synchronized object, you need a pointer to the thread that currently has the lock, a ready queue and a pointer to the waiting queue (including threads that need to be notified of the state change of the locked object).
You might ask, what happens when two threads call at the same time? No matter how close the two threads call are, there must actually be one in front and one in the back, so there will always be only one to obtain the object lock. Since it is an atomic operation, it is impossible for the CPU to prefer one thread instead of another. For better performance, you should delay the acquisition lock call of the latter thread and immediately release the object lock of the previous thread. Locking is feasible for private and internal objects, but for external objects it may cause deadlocks because irrelevant code may lock the same object for different purposes.
If you want to lock a piece of code, the best thing is to add a statement that sets the lock in the try statement and put it in the finally statement. For locking of the entire snippet, you can set the synchronous value in its constructor using the MethodImplAttribute class. This is an alternative method, and when the locking method returns, the lock is released. If you need to release the lock quickly, you can use the Monitor class and C# lock declaration instead of the above method.
Let's look at a piece of code using the Monitor class:
public void some_method()
{
int a=100;
int b=0;
(this);
//say we do something here.
int c=a/b;
(this);
}
Running the above code will cause problems. When the code runs to int c=a/b; , an exception will be thrown and will not be returned. Therefore, this program will be suspended and other threads will not get the lock. There are two ways to solve the above problem. The first method is: put the code into try... finally and call it finally, so that the lock will be released in the end. The second method is: use the lock() method of C#. Calling this method has the same effect as calling it. However, once the code execution is out of range, releasing the lock will not occur automatically. See the code below:
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 and this method is used in situations where your code segment cannot be interrupted by other independent threads.
WaitHandle Class
The WaitHandle class is used as a base class, which allows multiple wait operations. This class encapsulates the synchronization processing method of win32. The WaitHandle object notifies other threads that it requires exclusive access to the resource, and other threads must wait until the WaitHandle no longer uses the resource and the wait handle is not used. Here are several classes inherited from it:
Mutex class: Synchronous primitives can also be used for inter-process synchronization.
AutoResetEvent: Notifies one or more threads waiting for an event that has occurred. This kind cannot be inherited.
ManualResetEvent: Appears when one or more waiting thread events have occurred. This kind cannot be inherited.
These classes define some signaling mechanisms to possess and release exclusive access to resources. They have two states: signed and nonsignaled. The waiting handle for the Signaled state does not belong to any thread unless it is a nonsignaled state. Threads that have waiting handles no longer use the waiting handles to use the set method. Other threads can call the Reset method to change the state or any WaitHandle method requires that they have a waiting handle. These methods are shown below:
WaitAll: Wait for all elements in the specified array to receive the signal.
WaitAny: Wait for any element in the specified array to receive the signal.
WaitOne: When overridden in the derived class, block the current thread until the current WaitHandle receives the signal.
These wait methods block the thread until one or more synchronous objects receive a signal.
The WaitHandle object encapsulates operating system-specific objects waiting for exclusive access to shared resources, whether they are managed or unmanaged code. But it is not as lightweight as Monitor, which is completely managed code and is very efficient in using operating system resources.
Mutex Class
Mutex is another method to complete inter-thread and cross-process synchronization, and it also provides inter-process synchronization. It allows one thread to exclusively occupy shared resources while blocking access to other threads and processes. The name of Mutex is a good example of its owner's exclusive possession of resources. Once a thread owns Mutex, the other threads that want Mutex will hang until the occupying thread releases it. Methods are used to release Mutex. A thread can call the wait method multiple times to request the same Mutex, but the same number of times must be called when releasing Mutex. If no thread owns Mutex, then the state of Mutex becomes signed, otherwise it is nosignaled. Once the Mutex state becomes signed, the next thread waiting for the queue will get the Mutex. The Mutex class corresponds to the CreateMutex of win32. The method of creating Mutex objects is very simple. The following methods are commonly used:
A thread can obtain ownership of Mutex by calling or or . If Mutex does not belong to any thread, the above call will make the thread own Mutex, and WaitOne will return immediately. But if there are other threads that have Mutex, WaitOne will fall into an indefinite wait until Mutex is obtained. You can specify parameters in the WaitOne method, i.e. the waiting time, to avoid waiting for Mutex indefinitely. Calling Close to act on Mutex will release possession. Once Mutex is created, you can obtain the Mutex handle through the GetHandle method and use it for the or method.
Here is an example:
public void some_method()
{
int a=100;
int b=20;
Mutex firstMutex = new Mutex(false);
();
//some kind of processing can be done here.
Int x=a/b;
();
}
In the example above, the thread creates a Mutex, but it does not declare that it has it at the beginning, and owns Mutex by calling the WaitOne method.
Synchronization Events
Synchronization time is some waiting handles used to notify other threads about what happened and resources are available. They have two states: signed and nonsignaled. AutoResetEvent and ManualResetEvent are such synchronization events.
AutoResetEvent Class
This class can notify one or more threads of events. When a waiting thread is released, it converts the state to signaled. Use the set method to change its instance state to signaled. However, once the waiting thread is notified to be signed, its turntable will automatically become nonsignaled. If no threads listen for events, the turntable will remain signed. This type cannot be inherited.
ManualResetEvent Class
This class is also used to notify one or more thread events that have occurred. Its status can be manually set and reset. Manual reset time will remain signed until its state is set to nonsignaled, or keep it to nonsignaled until its state is set to signaled. This class cannot be inherited.
Interlocked Class
It provides synchronization of shared variable access between threads. Its operations are atomic operations and are shared by threads. You can increase or decrease shared variables by or. Its point is that it is atomic operations, that is, these methods can increment the parameter of an integer and return a new value. All operations are one step. You can also use it to specify the value of the variable or check whether the two variables are equal. If it is equal, the specified value will be replaced by the value of one of the variables.
ReaderWriterLock class
It defines a lock, providing a unique write/multi-read mechanism, so that read and write synchronization. Any number of threads can read data, and the data lock will be required when there are threads to update data. The read thread can acquire the lock, if and only if there is no thread written here. When there is no read thread and other write threads, the write thread can get the lock. Therefore, once the writer-lock is requested, all read threads will not be able to read the data until the write thread is accessed. It supports pause and avoids deadlocks. It also supports nested read/write locks. The method to support nested read locks is that if a thread has a write lock, the thread will be paused;
The method to support nested write locks is that if a thread has a read lock, the thread will pause. If there is a read lock, it will easily be deadlocked. The safe way is to use the method, which will upgrade the reader to the writer. You can use the method to downgrade the writer to the reader. The call will release the lock and reload the state of the lock until it is called.
in conclusion:
This part talks about the problem of thread synchronization on the .NET platform. 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 programs, we should be better off using these methods carefully. Otherwise, it will not bring benefits, but will lead to performance degradation or even program crashes. Only a large number of connections and experiences can enable you to master these techniques. Try to use things that cannot be completed or are uncertainly blocked in the synchronized code block, especially I/O operations; use local variables as much as possible instead of global variables; synchronization is used where part of the code is accessed by multiple threads and processes and the state is shared by different processes; arrange your code so that each data is accurately controlled in one thread; it is safe to not share the code between threads; in the next article, we will learn about thread pool knowledge.