- In .Net there is
- the lock statement
- and the Monitor class
to synchronize access to common critical sections of code by concurrent threads.
- Sometimes there are use cases where simultaneous read access is allowed but the writing process has to be synchronized. This is done with the ReaderWriterLock and ReaderWriterLockSlim classes.
- You should secure access to simple field values with the Interlocked class.
The use of thread locks, however, harbors the risk of deadlocks. The larger the code sections and the deeper the call stacks that a thread lock encompasses, the higher the risk. Therefore, when programming, you should always remember to keep the "critical sections" as short as possible.
The annoying thing about deadlocks is that they are discovered late and usually when the software is running in productive operation. In this situation only a restart of the service helps and this is often a very critical thing, depending on the area of application.
For this reason there is the class gip.core.datamodel.ACMonitor, which has the following advantages:
- Preventive detection of deadlock situations during the development phase using lock hierarchies "(also known as" lock leveling").
- Resolving deadlocks during the "maturity phase" or "stabilization phase" in production environments.
Preventive detection of deadlock situations
The concept of the .NET Monitor class provides that a field of type object is declared and passed when the Enter method is called. When using the ACMonitor class, pass an ACMonitorObject instead . The ACMonitorObject constructor requires an int value to be passed that indicates the lock level . Example:
public readonly ACMonitorObject _10020_LockValue = new ACMonitorObject (10020);
The number that you assign here should be as unique as possible and the size of the number should be adapted to the application level.
In iPlus, it is organized so that it is a five-digit number. The first digit corresponds to the assembly in which it is used. The smaller the number, the deeper the application layer:
Datalayer:
gip.core.datamodel: 10000
gip.mes.datamodel: 11000
Runtimelayer:
gip.core.autocomponent: 20000
Communicationlayer with other systems:
gip.core.communication: 30000
Managerlayer:
*.manager.dll: 40000
Applicationlayers:
*.processapplication*.dll: 60000
*bso*dll: 70000
The second to fifth positions are used for further level classification within the assemblies.
If you now use the lock objects, then a lock object from a lower level must never be used in front of a lock object from a higher level!
To prevent this from happening you need the ACMonitor class. It is used as follows:
using (ACMonitor.Lock(_11020_LockValue))
{
// Not allowed to use a lock with a higher number than 11020:
// SynchronizationLockException with be thrown in Debugger;
// otherwise the stacktrace dumped into the logfile
using (ACMonitor.Lock(_20010_LockValue))
{
}
}
In the above case, you as the programmer will be informed that you have not adhered to the lock hierarchy and that this is a potential danger of deadlock.
The notification is made in two ways:
The check of the lock hierarchies is activated using "static bool ACMonitor.ValidateLockHierarchy". ValidateLockHierarchy is either activated in the application config file in the CoreConfiguration section or you set this property directly in your application code .
Resolving deadlocks
Using lock hierarchies, you have already reduced the risk of a future deadlock situation enormously in the development phase. However, you can never fully simulate the dynamic behavior during the development phase. In productive operation, a lot comes together at once:
- Many users simultaneously interacting with the processes via the iPlus network layer,
- Communication with external systems (different communication protocols that run in other threads and asynchronously change the process states in the application trees)
- Other environmental conditions: More processor cores and memory; larger amounts of data to be processed; many business processes that have to be processed at the same time, ...
- Different patterns of call stacks due to the service orientation of the iPlus framework. This means that for example your program code from other instances is called (ACComponents party) because the customer purchased additional packages has.
You can use the "ValidateLockHierarchy" switch to output hierarchy violations in productive operation, but what happens if a deadlock occurs anyway?
You have to kill the iPlus service in the task manager because the two threads involved can no longer run and "Thread.Join()" cannot be completed even when shutting down. In addition, an abrupt termination of the iPlus service can have fatal consequences for business processes (especially in real-time industrial systems). Here it must be weighed up which problem is more critical:
- An inconsistent state of an ACComponent instance because two threads wanted to run through the critical section of code at the same time and an exception occurred so that there was no deadlock
- or a prolonged shutdown of a production plant, where even human lives may be at risk, because the iPlus service is stopped?
Your decision is already obvious here: Option A - You accept the inconsistent state because it can be restored through the many possibilities for influencing it via the client interface. Activate the "UseSimpleMonitor = false" switch for a certain period of time (eg for a few weeks) so that the entire system can go through a maturity phase or a stabilization phase.
This setting means that in the event of a deadlock, a SynchronizationLockException is thrown and diagnostic and stack information is output in the message log. The deadlock does not take place at all, but the call stack that could have led to the deadlock is resolved by the exception. The critical code sections are therefore not run through completely and the corresponding instance is then in an inconsistent state. The output in the message log must be examined here and forwarded to the responsible developer so that the program can be changed.
If the system has run deadlock-free for some time (without entries in the message log ), set UseSimpleMonitor to true . With this you declare that the "maturity phase" has been completed and that the system is now running stable. The result of this change is that the computationally expensive deadlock detection mechanism is deactivated and the efficient monitor class is used internally instead.