Thread synchronization

Top  Previous  Next

When developing multithreaded applications, one important issue to understand is the need for thread synchronization. In the previous example with the SMS alarm application, we actually did see the Channel thread synchronization primitive used. Another need for thread synchronization occurs when more than one thread accesses shared data - like global variables used by several threads.

 

The MUTEX (Mutual Exclusion) data type is the most important mechanism that is used for implementing critical sections in a multithreading VPL program. Critical sections are used in the cases where more than one thread is accessing the same shared resource - like a global variable/data structure accessed from several threads.

By using a Mutex, it is ensured that only one thread will be executing in the critical section at one time. Other threads that also want to enter the critical section will be blocked until the thread executing in the critical section leaves it.

 

Use of critical sections is important to avoid the so-called "lost update problem" that occurs because each piece of code assumes that between the point where it takes a copy of the data and the point where it writes it back, no other tasks will change the data.

The VPL line:

 

 Count:=Count + 1;

 

Will actually be executed as 3 machine code instructions as seen below:

 


LD

A,[Count]

; Load the Count value into register A


ADD

A,1

; Add 1 to the value of register A


ST

[COUNT],A

; Save register a to Count

 

 

If we assume that two threads are executing these instructions to increment the Count variable:

 

 

Thread A

 

Thread B

A1:

LD

A,[Count]

B1:

LD

B,[Count]

A2:

ADD

A,1

B2:

ADD

B,5

A3:

ST

[COUNT],A

B3:

ST

[COUNT],B

 

 

Consider the following sequence of execution:

 

A1, A2, task_switch, B1, B2, B3, task_switch, A3 …

 

What problem occurs here?

 

Thread-syncronization-lostupdate

 

 

Thread A will increment Count with 1 and Thread B will increment Count with 5. As Count is 10 initially, the correct answer after execution will be 16, but because of the lost update problem, the result is 11 instead.

 

By placing the instructions to increment the Count variable in a critical section, it will be assured that the load, increment, and store are executed in one atomic operation which will eliminate the lost update problem:

 

 


Thread A


Thread B

A0:

mxLock();

B0:

mxLock();

A1:

LD

A,[Count]

B1:

LD

B,[Count]

A2:

ADD

A,1

B2:

ADD

B,5

A3:

ST

[COUNT],A

B3:

ST

[COUNT],B

A4:

mxUnlock();

B4:

mxUnlock();

 

 

The sequence of execution is now:

 

A0, A1, A2, task_switch, B0, task_switch, A3, A4,task_switch, B1, B2, B3, B4 …

 

It can now be seen that by using a critical section, we have eliminated the lost update problem.

 

 

The short example below demonstrates how to protect the "Count" variable when accessed from 3 threads - the main thread and 2 threads dynamically created.

Without the use of critical sections implemented by a Mutex, the lost update problem will appear because the increment of the "Count" is composed of several machine code instructions that may be interrupted at any time.

 

 

INCLUDE rtcu.inc
 
VAR
  mxCnt : MUTEX;
  Count : DINT := 0;
END_VAR;
 
THREAD_BLOCK Thread_A;
WHILE TRUE DO
  mxLock(mx:=mxCnt);
  Count := Count + 1;
  mxUnlock(mx:=mxCnt);
END_WHILE;
END_THREAD_BLOCK;
 
 
THREAD_BLOCK Thread_B;
WHILE TRUE DO
  mxLock(mx:=mxCnt);
  Count := Count + 5;
  mxUnlock(mx:=mxCnt);
END_WHILE;
END_THREAD_BLOCK;
 
 
PROGRAM test;
VAR
  TA   : Thread_A;
  TB   : Thread_B;
  i     : INT;
END_VAR;
 
mxCnt := mxInit();
 
TA();
TB();
 
BEGIN
  Sleep(delay:=1000);
  DebugFmt(message:="Count: \4",v4:=Count);
END;
 
END_PROGRAM;

 

 

Another synchronization mechanism is the Semaphore.

 

Semaphores are a programming construct designed by E. W. Dijkstra in the late 1960s. Dijkstra's model was the operation of railroads. Consider, for example, a stretch of railroad in which there is a single track over which only one train at a time is allowed. Guarding this track is a semaphore. A train must wait before entering the single track until the semaphore is in a state that permits travel. When the train enters the track, the semaphore changes state to prevent other trains from entering the track. A train that is leaving this section of track must again change the state of the semaphore to allow another train to enter.

 

 

One classical example of the use of Semaphores is the so-called "Producer/Consumer problem" where a producer thread produces data to a consumer thread, and access to the shared buffer in between must be synchronized. In the RTCU environment, a CHANNEL could have been used to solve this problem but a more low-level implementation using three Semaphores could look like this:

 

 

INCLUDE rtcu.inc
 
VAR
  buffer   : ARRAY[0..4] OF INT;
  buf_in   : INT;
  buf_out : INT;
  crit     : SEMAPHORE;
  notempty : SEMAPHORE;
  full     : SEMAPHORE;
END_VAR;
 
THREAD_BLOCK Producer;
VAR
  elem : INT;
END_VAR;
WHILE TRUE DO
  Sleep(delay:=1000);
  elem:=elem+1;
  semWait(sem:=notempty);
  semWait(sem:=crit);
  buffer[buf_in]:=elem;
  buf_in:=(buf_in+1) MOD (SIZEOF(buffer)/SIZEOF(buffer[0]));
  DebugFmt(message:="Produced=\1",v1:=elem);
  semSignal(sem:=crit);
  semSignal(sem:=full);
END_WHILE;
END_THREAD_BLOCK;
 
THREAD_BLOCK Consumer;
VAR
  elem : INT;
END_VAR;
WHILE TRUE DO
  Sleep(delay:=1000);
  semWait(sem:=full);
  semWait(sem:=crit);
  elem:=buffer[buf_out];
  buf_out:=(buf_out+1) MOD (SIZEOF(buffer)/SIZEOF(buffer[0]));
  DebugFmt(message:="Consumed=\1",v1:=elem);
  semSignal(sem:=crit);
  semSignal(sem:=notempty);
END_WHILE;
END_THREAD_BLOCK;
 
PROGRAM test;
VAR
  P : Producer;
  C : Consumer;
END_VAR;
 
notempty:=semInit(initval:=SIZEOF(buffer)/SIZEOF(buffer[0]));
full:=semInit(initval:=0);
crit:=semInit(initval:=1);
P();
C();
 
BEGIN
END;
END_PROGRAM;

 

The notempty and full Semaphores are used for synchronizing the insertion and removal to the buffer, so even as the Producer is producing faster than the Consumer, they will be fully synchronized without any data loss.

The crit semaphore is used exactly with the same purpose as a Mutex, and, as it has been demonstrated, a MUTEX can also be implemented by using a Semaphore with the initial value of 1 - also called a binary semaphore

Semaphores are rarely used for implementing critical sections because using the Mutex is a safer and more elegant way to accomplish this.

 

 

Please read more about the different synchronization mechanism in their respective sections - MUTEX, SEMAPHORE and CHANNEL.

Also have a look at the RTCU Multithreading data sheet for the limitations present in the system.