Mutex procedure (TestAndSet, FB_IecCriticalSection) for securing critical sections

When using mutex procedures or implementing a mutual exclusion, the sections in which competing accesses occur are defined as critical sections. These sections can be synchronized using the function TestAndSet() or the function block FB_IecCriticalSection (both from the PLC library Tc2_System), so that the sections are placed under mutual exclusion and only one task can access the shared data at a time.

Entering a critical section may depend on one or more conditions. In addition, different critical sections can depend on different conditions.

Examples

  • If section 1a reads and section 1b writes to "Data1", sections 1a and 1b must be synchronized with each other. If sections 2a, 2b and 2c each have read and write access to the “Data2” data, sections 2a, 2b and 2c must be synchronized with each other.
    The ranges 1x and 2x each depend only on a condition (“Data1” or “Data2” not locked). In addition, in this case they are not interdependent, since different data is accessed in each section. This means that even if section 1b locks the “Data1” data, section 2a can simultaneously lock and access the “Data2” data.
    Therefore, the data resources must be synchronized between the following critical sections:
    • the use of resource “Data1” between sections 1a and 1b
    • the use of resource “Data2” between sections 2a, 2b and 2c
  • The situation changes if sections 1x and 2x additionally access the “DataA” data (at least one write access takes place).
    Then all sections 1x and 2x are dependent on two conditions: To enter section 1x, data “Data1” and “DataA” have to be enabled, to enter section 2x, data “Data2” and “DataA” have to be enabled.
    In addition, sections 1x and 2x are now interdependent due to the shared use of the “DataA” data and may no longer be executed simultaneously.
    Therefore, the data resources must be synchronized between the following critical sections:
    • the use of resource “Data1” between sections 1a and 1b
    • the use of resource “Data2” between sections 2a, 2b and 2c
    • the use of the “DataA” resource between all sections (1a, 1b, 2a, 2b, 2c)

 
With the TestAndSet() function, a flag (Boolean variable) represents a condition on which entering the critical section depends (e.g. bLockData1). With FB_IecCriticalSection, one instance of the function block is used as a lock condition.

Blockage

  • TestAndSet(): The function can be used to select and check the content of a critical section. However, the function does not have a blocking effect, and it is possible that the section cannot be processed in a cycle.
  • FB_IecCriticalSection: If another task tries to access an occupied critical section by calling the Enter() method, it is blocked by the TwinCAT scheduler. The task is blocked until the section is enabled again.
CAUTION
Cycle timeout due to stopped task

The duration of the task blocking can lead to the task exceeding the cycle time, depending on the (utilization of the) cycle time.

  • Ensure that the critical sections are kept short, in order to avoid cycle overruns in the waiting task. If several tasks are waiting to enter the critical section, access is granted based on their priority.

This results in the advantage of the TestAndSet() function, compared to the function block FB_IecCriticalSection, that a task is not blocked under any circumstances. The disadvantage is that, in the event that the function TestAndSet() does not grant access, an alternative implementation must be available. This could, for example, be realized via a state machine in order to try access again in the next cycle.

Deadlock

Note that if the function block FB_IecCriticalSection is used unfavorably, the tasks may become locked, deadlocks may occur. A deadlock always involves at least two tasks. It occurs when the tasks are waiting for each other to release another resource that the other task has already locked.

CAUTION
Permanent task standstill due to deadlock

Once a deadlock has occurred as described, it can no longer be eliminated programmatically. The tasks involved come to a permanent standstill.

Example:

  • Tasks 1 and 2 both require access to the “DataA” and “DataB” data.
  • Task 1 initially locks the resource “DataA” and task 2 simultaneously locks the resource “DataB”.
  • Task 1 then wants to lock the “DataB” data. Since this resource is already locked by Task 2, this is not possible and Task 1 is locked.
  • Task 2, in turn, attempts to lock the “DataA” in addition to “DataB”. Since this is already locked by Task 1, this locking is also not possible and Task 2 is also locked.
  • Thus, both tasks are locked. The tasks then waiting for the release of data that the other task locks but cannot release due to its own locking. As a result, the two tasks wait indefinitely for each other.

Even with incorrect synchronization, a deadlock situation is not inevitable. A deadlock only occurs if the processes occur randomly in an unfavorable sequence.

Avoiding deadlocks

In general, you can avoid deadlocks if each task only wants to lock one resource at a time.

You can also avoid deadlocks by only ever requesting and locking resources in a certain sequence.

Example:

  • The problem with the above example is that Task 1 and Task 2 request and lock the “DataA” and “DataB” resources in a different order.
  • However, if both tasks always lock the resources in the same order, the deadlocking problem is avoided. This means: If each task that requires access to “DataA” and “DataB” in a critical section always requests “DataA” first and, if possible, locks it and only requests and, if possible, locks the “DataB” resource once “DataA” is successfully locked, there can be no deadlock where the tasks “snatch away” the required resources in different sequences as described above.

Sample program: Access synchronization using TestAndSet()

This example shows how shared data can be accessed securely from different task contexts. The data is combined in a structure instance. This also contains a Boolean variable bLocked as a test flag.

Before you read or write to data of this global structure instance, you must request access to it by calling the function TestAndSet() with the corresponding test flag. If access is not granted, you cannot access the data in this cycle. In this case, an alternative treatment must be provided for. If necessary, access is requested again in the next cycle. If access is granted and TestAndSet() was called successfully, you can read or modify the data. As soon as you have finished editing, release the access again by setting the test flag to FALSE.

Function test

Start the sample program. Within MAIN1 and MAIN2 there is a counter variable (nLocalBlockedCounter), which increments if the TestAndSet() call fails. If the counter is 0, the data access to the global variables was successfully executed on each attempt.

To get a feel for the need for access synchronization, you can run the two tasks from different CPU cores. If you now start the sample program, you will see how the counter variables are incremented irregularly, indicating that data access was occasionally blocked.

Access synchronization is not only necessary for multi-core use, but also when the two tasks run on the same CPU. Mutual task interruptions can also lead to critical inconsistencies in unsaved data access.

Download:TC3_PlcSample_MultiTaskSync_TestAndSet.zip.

Sample program: Access synchronization using FB_IecCriticalSection

Windows CE

The functionality of the FB_IecCriticalSection is supported under Windows CE operating systems from TwinCAT v3.1.4022.29 onwards.

The sample shows the use of Critical Sections in the PLC using money transfers for cash accounts. An account is represented by a function block FB_Account. Four accounts are involved. All four function block instances are declared in a global variable list to enable access (here: money transfer) from different task contexts.

Each account has an initial balance of 1000. The balance of each account can be read and reset using the Get() and Set() methods. The following money transfers are implemented in four different task contexts.

Task 1: A->B 500

Task 2: B->C 250, B->D 250

Task 3: C->A 250

Task 4: E->A 250

Once these money transfers have been completed, each account should have a balance of 1000.

It must be ensured that access to an account never occurs from two task contexts at the same time.

The function block FB_IecCriticalSection is used in the FB_Account. The method FB_Account.Lock() executes Enter() on the Critical Section, the method FB_Account.Unlock() executes Leave() on the Critical Section. Before an account balance can be accessed, the Lock() method must be executed successfully. Access to this account is then blocked for others.

To avoid a deadlock, a locking sequence is defined. This should be defined as follows:

Locking sequence: A before B before C before D

This results in the following unlocking sequence: D before C before B before A

Implementation of money transfer within Task 3:

(* Task 3: C->A 250*)
IF GVL.fbDepotA.Lock() THEN
    IF GVL.fbDepotC.Lock () THEN
        GVL.fbDepotC.Set (GVL.fbDepotC.Get() - 250);
        GVL.fbDepotA.Set (GVL.fbDepotA.Get() + 250);
        GVL.fbDepotC.Unlock();
    END_IF
    GVL.fbDepotA.Unlock();
END_IF

Function test

Start the sample program. Within Main1 you can see the sum of all account balances, which must always be 4000 in PLC online view.

To get a feel for the need to use Critical Sections in this example, you can run the four tasks from different CPUs and set the global variable bIgnoreLock to TRUE. If you now start the sample program, you will see how the account balances assume incorrect values and the sum of all account balances also indicates a money transfer malfunction.

Access synchronization is not only necessary for multi-core use, but also when the four tasks run on the same CPU core. Mutual task interruptions can also lead to critical inconsistencies in unsaved data access.

Download: TC3_PlcSample_MultiTaskSync_IecCriticalSection.zip