Advanced programming


Multi-threading is an important aspect in iPlus programming. Always try to parallelize tasks whenever possible for the following reasons:

  1. Calls from proxy components to real components (on the server side) do not result in the client-side thread being blocked. For example that the surface on the client side freezes.
  2. Many components that communicate with other machines and devices in the network are often used in iPlus projects (e.g. PLC connections via TCP / IP, Modbus, OPC, http-based services such as SOAP or REST / JSON, FTP, SFTP, or other proprietary protocols). Instances from the application trees are informed by these communication instances using event technology when values ​​or states have changed. The event handlers in the application instances should delegate their application logic to other threads so that the communication thread is not blocked. Long blocks lead to a standstill of the User-Interface on the client side and, on the other hand, other application instances have to wait a long time before being informed.
  3. Better overall processor utilization.

Conversely, multi-threaded programming means that access to shared resources must be protected by thread locks . In addition, the use of thread locks harbors the risk of deadlocks, which is a particular challenge for many programmers.

In the iPlus framework, however, some mechanisms are implemented that help you to implement the aforementioned problems and challenges more easily:

  1. Use of the special ACThread class to perform performance analyzes during runtime.
  2. Use of so-called "lock hierarchies" (also known as "lock leveling") using the ACMonitor and ACMonitorObject classes.
  3. Use of  ACDelegateQueue to put tasks in a queue that are processed in separate threads.

 

 

The gip.core.datamodel.ACThread class is a class that contains a encapsulated .NET thread class. Always use this class, so that the iPlus runtime is able to generate performance statistics. Statistics are created internally via a static instance of the "gip.core.datamodel.PerformanceLogger" class. You output these thread statistics with the RuntimeDump class.

The following example shows how to instantiate and use the ACThread class:

 

  1. First you need an instance of ManualResetEvent. ManualResetEvent is required to be able to terminate a thread cleanly by sending a signal from the main thread to the worker thread so that it can break off its working endless loop (see point 4).
  2. You need an instance of ACThread. When instantiating, pass the working method, that is to be called when the thread is started and that contains an endless loop in which the actual application logic is to be executed. In the example above, this is the RunWorkCycle() method.
  3. You only start the new thread in the ACInit method. Before doing this, you should definitely set the Name property with a unique, human-readable identifier. It is best to always use the ACUrl of the ACComponent instance for this.
  4. Insert an endless loop in the working method, which is active until the ManualResetEvent instance has received a signal. To do this, call the Wait method within the While body. Either the Wait method waits for the waiting time passed and comes back with True, or it comes back immediately with False if it has received a termination signal from the main thread.
  5. Within the working loop, always call "StartReportingExeTime()" first to start the time measurement for the thread statistics. StartReportingExeTime() returns a PerformanceEvent instance which is basically a "System.Diagnostics.Stopwatch".
  6. Then comes your application code, which is symbolized in the example above by the DoSomething() method.
  7. At the end you stop the "StopReportingExeTime()" time measurement.
  8. You terminate the worker thread in the ACDeInit method  by first calling the Set method of the ManualResetEvent instance to send the termination signal to the worker thread.
  9. Then call the Join() method ("Thread.Join()" is called internally ). The join method waits until the application code in the worker thread has been fully executed and has left the endless loop. The thread is then terminated. If the transferred timeout time is not sufficient to execute the application code, the termination is forced via Abort(). In this case, a warning is written in the message log that the join time was insufficient. In this case, the application developer must investigate and correct the cause. So make sure, that your application code is not blocked, so that the Join() method can terminate a thread in a regular manner.

 


You always need your own threads if you want to call a certain code cyclically at the same time intervals. However, if you have different code that you just want to delegate to another thread so that the calling thread is not blocked, then use the "gip.core.datamodel. ACDelegateQueue" class. ACDelegateQueue is comparable to the task class in ".NET". ACDelegateQueues, however, have the advantage that only one explicit ACThread is used and that the tasks are placed in a queue that are processed again in the same order. Delegate queues can be diagnosed using RuntimeDump to create performance statistics by using the ACThread-class. In addition, ACDelegateQueues can also be stopped and restarted ("RestartQueue()" method).

ACDelegateQueue is also the basic class of database queues that you have already got to know in the database chapter.

The following example shows you how to use ACDelegateQueues:

 

  1. Instantiate the ACDelegateQueue in the ACInit() method by passing a unique identifier. As with ACThreads, it is best to pass the ACUrl.
  2. Call the Add() method to pass your delegate.
  3. Call StopWorkerThread() in the ACDeInit method to terminate the queue.

 


The work thread in an ACDelegateQueue only ever becomes active as soon as a new task is placed in the queue. It spends the rest of the time sleeping so that computing power is not used up unnecessarily.

If you want to use in the ACThread like in the first example and do not want to that the method "DoSomething()" is called cyclic (even if it is nothing to do), then use instead ManualResetEvent the class  gip.core.datamodel. SyncQueueEvents:

 

  1. First you need an instance of gip.core.datamodel.SyncQueueEvents. SyncQueueEvents is required to be able to terminate a thread cleanly by sending a signal  from the main thread to the worker thread so that it can break off its working endless loop (see point 4).
  2. You need an instance of ACThread. When instantiating, pass the working method that is to be called when the thread is started and that contains an endless loop in which the application logic is to be executed. In the example above, this is the RunWorkCycle() method.
  3. You only start the new thread in the ACInit method. Before doing this, you should definitely set the Name property with a unique, human-readable identifier. It is best to always use the ACUrl of  the ACComponent instance for this.
  4. Insert an endless loop in the working method, which is active until the SyncQueueEvents instance has received a signal. To do this, call the WaitOne method within the While body Either the WaitOne method waits for the passed waiting time of 0 seconds and comes back with True, or it comes back immediately with False if it has received a termination signal from the main thread.
  5. Call the NewItemEvent.WaitOne() method. This puts the worker thread to sleep.
  6. Only when the NewItemEvent.Set() method is called, the worker thread is woken up again.
  7. Within the working loop, always call "StartReportingExeTime()" first to start the time measurement for the thread statistics. StartReportingExeTime() returns a PerformanceEvent instance which is basically a "System.Diagnostics.Stopwatch".
  8. Then comes your application code, which is symbolized in the example above by the DoSomething() method.
  9. At the end you stop the "StopReportingExeTime()" time measurement.
  10. You terminate the worker thread in the ACDeInit method  by first calling the TerminateThread() method of the SyncQueueEvents instance to send the termination signal to the worker thread.
  11. After the application code in the worker thread has been executed completely and has left the infinite loop, call ThreadTerminated() to signal the main thread that the worker thread has done its job.
  12. The Join() method ("Thread.Join ()" is called internally) waits for step 11 and then terminates the worker thread.

  1. In .Net there is
    • the lock statement
    • and the Monitor class
      to synchronize access to common critical sections of code by concurrent threads.
  2. 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.
  3. 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:

  1. Preventive detection of deadlock situations during the development phase using  lock hierarchies "(also known as" lock leveling").
  2. 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:

  1. Many users simultaneously interacting with the processes via the iPlus network layer,
  2. Communication with external systems (different communication protocols that run in other threads and asynchronously change the process states in the application trees)
  3. 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, ...
  4. 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:

  1. 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
  2. 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.