User Tools

Site Tools


mtsynchronization_objects.htm
Navigation:  Advanced Topics > Thread Model Documentation > Multi-Threading Programming >====== Synchronization Objects ====== Previous pageReturn to chapter overviewNext page

A Synchronization Object is an object used to control how multiple threads cooperate in a preemptive environment. There are four Synchronization Objects supplied by the Clarion runtime: Critical Sections (ICriticalSection), Mutexes (IMutex), Semaphores (ISemaphore), and Read/Write Locking (IReaderWriterLock).

Clarion 6 makes it very easy to take advantage of preemptive threads in your applications. All template generated code uses threaded objects to ensure proper behavior. When you embed code that works with threaded data you don't have any worries, but when you access shared non-threaded data you should use a synchronization object.

 

NoteBox.jpg

Due to the fact that Windows uses procedure-modal methods when dealing with MDI based applications (a program with an APPLICATION window) you must not have any user input when you have control of a synchronization object with an MDI based application. This is likely to lead to your application locking up

If you must have user input, then you must release control of the synchronization object while waiting for user input.

 

IcriticalSection and CriticalSection

Use an ICriticalSection when you want only one thread accessing some resource (e.g. a global, non-threaded variable) at any one time. An ICriticalSection is faster than an IMutex. If you do not need the extra features of an IMutex, use an IcriticalSection

CriticalSection is a built-in class that allows for easy creation of simple, global synchronization objects.

Following are a couple of examples that make sure that only one thread is accessing a static queue at a time.

PROGRAM

! This program assumes that only WriteToQueue and

! ReadFromQueue directly access NonThreadedQueue

! If other code accesses the queue and does not use

! the QueueLock critical section to synchronize

! access to the queue, then all the work inside WriteToQueue

! and ReadFromQueue is wasted

QueueData GROUP,THREAD

ThreadID    LONG

Information STRING(20)

END

!Include CriticalSection

INCLUDE('CWSYNCHC.INC'),ONCE

MAP

WriteToQueue()

WriteToQueue(*QueueData)

ReadFromQueue()

ReadFromQueue(*QueueData)

END

NonThreadedQueue QUEUE

Data               LIKE(QueueData)

END

QueueLock CriticalSection

CODE

! Do everything

WriteToQueue PROCEDURE()

! Assumes QueueData is used to pass data.  This is thread safe

! because QueueData has the THREAD attribute

CODE

QueueLock.Wait()  !Lock access to NonThreadedQueue.

NonThreadedQueue.Data = QueueData

GET(NonThreadedQueue, NonThreadedQueue.Data.ThreadId)

IF ERRORCODE()

ADD(NonThreadedQueue)

ELSE

PUT(NonThreadedQueue)

END

QueueLock.Release() !Allow other access to the queue

WriteToQueue PROCEDURE(*QueueData in)

CODE

QueueLock.Wait()  !Lock access to NonThreadedQueue.

NonThreadedQueue.Data = in

GET(NonThreadedQueue, NonThreadedQueue.Data.ThreadId)

IF ERRORCODE()

ADD(NonThreadedQueue)

ELSE

PUT(NonThreadedQueue)

END

QueueLock.Release() !Allow other access to the queue

ReadFromQueue PROCEDURE()

! Returns results in QueueData.  This is thread safe

! because QueueData has the THREAD attribute

CODE

QueueLock.Wait()  !Lock access to NonThreadedQueue.

NonThreadedQueue.Data.ThreadId = THREAD()

GET(NonThreadedQueue, NonThreadedQueue.Data.ThreadId)

QueueData = NonThreadedQueue.Data

QueueLock.Release() !Allow other access to the queue

ReadFromQueue PROCEDURE(*QueueData out)

CODE

QueueLock.Wait()  !Lock access to NonThreadedQueue.

NonThreadedQueue.Data.ThreadId = THREAD()

GET(NonThreadedQueue, NonThreadedQueue.Data.ThreadId)

out = NonThreadedQueue.Data

QueueLock.Release() !Allow other access to the queue

The previous example suffers from the problem that anyone can access the global queue and they are not forced to use the matching QueueLock critical section. The following example removes this problem by moving the non-threaded queue and the critical section into a static class.

PROGRAM

QueueData GROUP,THREAD

ThreadID    LONG

Information STRING(20)

END

NonThreadedQueue QUEUE,TYPE

Data               LIKE(QueueData)

END

QueueAccess CLASS

QueueData &NonThreadedQueue,PRIVATE

QueueLock &ICriticalSection,PRIVATE

Construct     PROCEDURE

Destruct      PROCEDURE

WriteToQueue  PROCEDURE

WriteToQueue  PROCEDURE(*QueueData)

ReadFromQueue PROCEDURE()

ReadFromQueue PROCEDURE(*QueueData)

END

INCLUDE('CWSYNCHC.INC')

MAP

END

CODE

! Do everything

QueueAccess.Construct PROCEDURE

CODE

SELF.QueueLock &= NewCriticalSection()

SELF.QueueData &= NEW(NonThreadedQueue)

QueueAccess.Destruct PROCEDURE

CODE

SELF.QueueLock.Kill()

DISPOSE(SELF.QueueData)

QueueAccess.WriteToQueue PROCEDURE()

! Assumes QueueData is used to pass data. This is thread safe

! because QueueData has the THREAD attribute

CODE

SELF.QueueLock.Wait()  !Lock access to NonThreadedQueue.

SELF.QueueData.Data = QueueData

GET(SELF.QueueData, SELF.QueueData.Data.ThreadId)

IF ERRORCODE()

ADD(SELF.QueueData)

ELSE

PUT(SELF.QueueData)

END

SELF.QueueLock.Release() !Allow other access to the queue

QueueAccess.WriteToQueue PROCEDURE(*QueueData in)

CODE

SELF.QueueLock.Wait()  !Lock access to NonThreadedQueue.

SELF.QueueData.Data = in

GET(SELF.QueueData, SELF.QueueData.Data.ThreadId)

IF ERRORCODE()

ADD(SELF.QueueData)

ELSE

PUT(SELF.QueueData)

END

SELF.QueueLock.Release() !Allow other access to the queue

QueueAccess.ReadFromQueue PROCEDURE()

! Returns results in QueueData.  This is thread safe

! because QueueData has the THREAD attribute

CODE

SELF.QueueLock.Wait()  !Lock access to NonThreadedQueue.

SELF.QueueData.Data.ThreadId = THREAD()

GET(SELF.QueueData, SELF.QueueData.Data.ThreadId)

QueueData = SELF.QueueData.Data

SELF.QueueLock.Release() !Allow other access to the queue

QueueAccess.ReadFromQueue PROCEDURE(*QueueData out)

CODE

SELF.QueueLock.Wait()  !Lock access to NonThreadedQueue.

SELF.QueueData.Data.ThreadId = THREAD()

GET(SELF.QueueData, SELF.QueueData.Data.ThreadId)

out = SELF.QueueData.Data

SELF.QueueLock.Release() !Allow other access to the queue

IWaitableSyncObject

The IWaitableSyncObject is the base interface for IMutex and ISemaphore. This allows you to create procedures that work with either type of synchronization object without requiring the procedure to know exactly what type of object it is.

IMutex

An IMutex is used when you want to allow only one thread to access a resource. Just like an ICriticalSection. However, IMutexes have the added features of being able to not only synchronize threads, but also synchronize different processes. Thus, if you have a resource that can only have one process accessing it at one time (e.g. a registration file that controls access to multiple programs) then you will need to use an IMutex that is created by calling NewMutex(Name). Name must be the same for all processes that use it to access the same

set of shared resources.

Another time you would use an IMutex rather than an ICriticalSection is if you do not want to always lock a thread.

Finally, an IMutex works better than an ICriticalSection in MDI applications, as ICriticalSection objects may cause deadlocks. Orphaned mutexes are killed by the operating system after a brief time, but CriticalSections are not. Deadlocks in CriticalSections are usually caused by a programming error (e.g., calling Wait before a form procedure and Release after it).

A Mutex is a very simple way to limit the user to having one instance of your program running at any time. The following example shows the use of the Name parameter for creating a Mutex and the TryWait method to limit your program in this way.

PROGRAM

INCLUDE('CWSYNCHM.INC'),ONCE

MAP

END

Limiter &IMutex,AUTO

Result  SIGNED,AUTO

LastErr LONG,AUTO          !<;<; return error

 CODE

 Limiter &amp;= NewMutex('MyApplicationLimiterMutex',,LastErr)

 IF Limiter &amp;= NULL

  MESSAGE ('ERROR: Mutex can not be created ' &amp; LastErr)

 ELSE

  Result = Limiter.TryWait(50)

   IF Result <;= WAIT:OK

    !Do Everything

    Limiter.Release()     !release

   ELSIF Result = WAIT:TIMEOUT

    MESSAGE('Timeout')

   ELSE

    MESSAGE('Waiting is failed ' &amp; Result) !show Result

   END

  Limiter.Kill()

 END

 

The difference between an IMutex and an ISemaphore is an IMutex can only have one thread successfully wait. An ISemaphore can have multiple threads successfully wait. It is also possible to create a semaphore where no thread can successfully wait.

ISemaphore with multiple successful waits

An ISemaphore created with an initial thread count other than zero will allow you to call Wait that number of times before the wait will lock. For example a semaphore created with MySem &amp;= NewSemaphore(,2) will allow MySem.Wait() to succeed twice without any call to MySem.Release(). This is a very easy way to limit the number of threads you have active at any one time.

If at any time you want to allow more calls to Wait to succeed, you can make additional Release() calls. The number of extra threads that can be added in this way is limited by the final parameter of NewSemaphore(). If you do not want to allow this feature, do not specify the final a maximum.

See the following section for a semaphore that limits the number of threads of a specific type to one.

ISemaphore with no waits

A semaphore created with an initial thread count of 0 will block any call to Wait until a Release is called. You use this type of semaphore to signal another thread that they can do something. For example, signal a thread to send an email to someone because you have sold the last candy bar.

Following is an example where the no wait style of semaphore is used to signal a reader that there is something to read. A multiple successful waits semaphore is used to limit the number of reader threads to 1.

PROGRAM

INCLUDE('CWSYNCHM.INC')

INCLUDE('CWSYNCHC.INC')

INCLUDE('ERRORS.CLW')

MAP

Reader()

Writer()

END

LogFile FILE,DRIVER('ASCII'),CREATE,NAME('LogFile.txt'),THREAD

RECORD

Line        STRING(255)

END

END

AccessToGlobals CriticalSection

NewData         Semaphore

LimitReaders    &amp;ISemaphore

GlobalStrings   QUEUE,PRE(Q)

Data              STRING(50)

END

AppFrame APPLICATION('Reader/Writer'),AT(,,400,240),SYSTEM,MAX,RESIZE

MENUBAR

MENU('&amp;File'),USE(?FileMenu)

ITEM('E&amp;xit'),USE(?Exit),STD(STD:Close)

END

MENU('&amp;Launch'),USE(?LaunchMenu)

ITEM('Reader'),USE(?LaunchReader)

ITEM('Writer'),USE(?LaunchWriter)

END

END

END

CODE

LimitReaders &amp;= NewSemaphore(1)

SHARE(LogFile)

IF ERRORCODE() = NoFileErr

CREATE(LogFile)

SHARE(LogFile)

END

IF ERRORCODE()

STOP('Log File could not be opened.  Error: ' &amp; ERROR())

END

OPEN(AppFrame)

ACCEPT

IF EVENT() = EVENT:Accepted

CASE ACCEPTED()

OF ?LaunchReader

START(Reader)

OF ?LaunchWriter

START(Writer)

END

END

END

! Test to see if Reader is still alive

IF LimitReaders.TryWait(1) = WAIT:TIMEOUT

!It is, so lets kill it

AccessToGlobals.Wait()

Q:Data = 'exit'

ADD(GlobalStrings)

AccessToGlobals.Release()

NewData.Release() ! Release the reader that is waiting

LimitReaders.Wait() ! Wait for thread to terminate

LimitReaders.Release()

END

LimitReaders.Kill()

Reader PROCEDURE

CODE

! Check that there are no other readers

IF LimitReaders.TryWait(1) = WAIT:TIMEOUT

MESSAGE('Only One Reader Allowed at a time.  ' &amp;|

'Kill reader by typing exit in a sender')

RETURN

END

! If TryWait succeeds, then we have control of

! the LimitReaders Semaphore

SHARE(LogFile)

l1  LOOP

NewData.Wait() !Wait until a writer signals that there is some data

AccessToGlobals.Wait()

LOOP

GET(GlobalStrings,1)

IF ERRORCODE() THEN BREAK.

IF Q:Data = 'exit' THEN

FREE(GlobalStrings)

AccessToGlobals.Release()

BREAK l1

ELSE

LogFile.Line = Q:Data

ADD(LogFile)

END

DELETE(GlobalStrings)

END

AccessToGlobals.Release()

END

CLOSE(LogFile)

LimitReaders.Release() !Allow a new Reader

RETURN

Writer PROCEDURE

LocString STRING(50)

Window WINDOW('Writer'),AT(,,143,43),GRAY

PROMPT('Enter String'),AT(2,9),USE(?Prompt1)

ENTRY(@s50),AT(45,9,95,10),USE(LocString)

BUTTON('Send'),AT(2,25,45,14),USE(?Send)

BUTTON('Close'),AT(95,25,45,14),USE(?Close)

END

CODE

OPEN(Window)

Window{PROP:Text} = 'Writer ' &amp; THREAD()

ACCEPT

IF EVENT() = EVENT:Accepted

CASE ACCEPTED()

OF ?Send

AccessToGlobals.Wait()

Q:Data = LocString

ADD(GlobalStrings)

AccessToGlobals.Release()

NewData.Release() ! Release the reader that is waiting

OF ?Close

BREAK

END

END

END

RETURN

IReaderWriterLock

An IReaderWriterLock can be used to allow multiple threads to read from a global resource, but for only one thread to write to it. No reader is allowed to read the resource if someone is writing and no one can write to the resource if anyone is reading.

An example of this would be where the user specifies screen colors. This is stored in an INI file and read on startup. As the user can change these at any time, the code that changes the values needs to obtain a write lock and all those that read the color information need to obtain a read lock.

Static queues cannot be synchronized via an IReaderWriterLock because reading a queue also modifies its position. All queue access must be considered to cause writes.

Here is some simple code that reads and writes to some static variables. To make sure that no one accesses the statics outside the locking mechanism, the variables are declared as PRIVATE members of a static class.

PROGRAM

INCLUDE('CWSYNCHM.INC'),ONCE

MAP

END

GlobalVars CLASS

AccessToGlobals     &amp;IReaderWriterLock,PRIVATE

BackgroundColor     LONG,PRIVATE

TextSize            SHORT,PRIVATE

Construct           PROCEDURE

Destruct            PROCEDURE

GetBackground       PROCEDURE(),LONG

PutBackground       PROCEDURE(LONG)

GetTextSize         PROCEDURE(),SHORT

PutTextSize         PROCEDURE(SHORT)

END

CODE

GlobalVars.Construct           PROCEDURE

CODE

SELF.AccessToGlobals &amp;= NewReaderWriterLock()

GlobalVars.Destruct            PROCEDURE

CODE

SELF.AccessToGlobals.Kill()

GlobalVars.GetBackground PROCEDURE()

ret SHORT,AUTO

Reader &amp;ISyncObject

CODE

! You need to copy the static variable

! to somewhere safe (A local variable) which

! can then be returned without fear that

! another thread will change it

Reader &amp;= SELF.AccessToGlobals.Reader()

Reader.Wait()

ret = SELF.Background

Reader.Release()

RETURN ret

GlobalVars.PutBackground PROCEDURE(SHORT newVal)

Writer &amp;ISyncObject

CODE

Writer &amp;= SELF.AccessToGlobals.Writer()

Writer.Wait()

SELF.Background = newVal

Writer.Release()

GlobalVars.GetTextSize         PROCEDURE()

ret SHORT,AUTO

Reader &amp;ISyncObject

CODE

! You need to copy the static variable

! to somewhere safe (A local variable) which

! can then be returned without fear that

! another thread will change it

Reader &amp;= SELF.AccessToGlobals.Reader()

Reader.Wait()

ret = SELF.TextSize

Reader.Release()

RETURN ret

GlobalVars.PutTextSize         PROCEDURE(SHORT newVal)

Writer &amp;ISyncObject

CODE

Writer &amp;= SELF.AccessToGlobals.Writer()

Writer.Wait()

SELF.TextSize = newVal

Writer.Release()

CriticalProcedure

The CriticalProcedure class is a very easy way to use an ISyncObject interface. If you create a local instance of a CriticalProcedure and initialize it, then it will look after the waiting for a lock and releasing the lock on the ISyncObject for you. The main advantage of using the CriticalProcedure class to handle the locking and releasing for you is that if you have multiple RETURN statements in your procedure, you do not have to worry about releasing the lock before each one. The destructor of the CriticalProcedure will handle that for you.

For example, the following code

PROGRAM

MAP

WRITETOFILE()

END

INCLUDE('CWSYNCHM.INC')

INCLUDE('CWSYNCHC.INC')

ERRORFILE FILE,DRIVER('ASCII'),PRE(EF)

RECORD     RECORD

LINE        STRING(100)

END

END

LOCKER &amp;ICRITICALSECTION

CODE

Locker &amp;= NewCriticalSection()

ASSERT(~Locker &amp;= Null)

!do everything

WriteToFile PROCEDURE()

CODE

Locker.Wait()

OPEN(ErrorFile)

IF ERRORCODE()

Locker.Release()

RETURN

END

SET(ErrorFile)

NEXT(ErrorFile)

IF ERRORCODE()

Locker.Release()

RETURN

END

EF:Line = 'Something'

PUT(ErrorFile)

CLOSE(ErrorFile)

Locker.Release()

Can be shortened, and made less error prone, to this:

PROGRAM

MAP

WRITETOFILE()

END

INCLUDE('CWSYNCHM.INC')

INCLUDE('CWSYNCHC.INC')

ERRORFILE FILE,DRIVER('ASCII'),PRE(EF)

RECORD     RECORD

LINE        STRING(12)

END

END

LOCKER &amp;ICRITICALSECTION

CODE

Locker &amp;= NewCriticalSection()

ASSERT(~Locker &amp;= Null)

!do everything

WriteToFile PROCEDURE()

CP CriticalProcedure

CODE

CP.Init(Locker)

OPEN(ErrorFile)

IF ERRORCODE()

RETURN

END

SET(ErrorFile)

NEXT(ErrorFile)

IF ERRORCODE()

RETURN

END

EF:Line = 'Something'

PUT(ErrorFile)

CLOSE(ErrorFile)

A second feature of the CriticalProcedure is that it allows you to RETURN a global (or other non-threaded static) variable from a prototype that requires protection. The return value will be pushed onto the return stack, and then during cleanup the Destructor will release the SyncObject. Another other way to RETURN a global string is to copy it to a local variable, release the SyncObject and RETURN the local string.

NoteBox.jpg

Since the base synchronization objects (CriticalSection and Mutex) support nested calls

(wait-wait-release-release), the operating system keeps a wait count, and you must RELEASE as

many times as you WAIT. The CriticalProcedure does not support nested calls (e.g., Init-Init-Kill-Kill). The second call to CriticalProcedure.Init will release the currently locked object (with a CP.Kill), and then try to lock the newly named object. There is no check made the exact same object is being “relocked”.

So, this will work with a Critical SECTION:

CrSec  &amp;ICriticalSection

 …

 CrSec.Wait

   ….

   CrSec.Wait

      … release count now 2 ..

   CrSec.Release

   … release count 1 ..

   .. code that needs protection ..

 CrSec.Release

Similar code with a Critical PROCEDURE will not

CrPro  CriticalProcedure

 …

 CrPro.Init(CrSec)

  ….

  CrPro.Init(CrSec)!CrSec is Released and Relocked so could lose the lock here

   … release count is 1 and not 2 ..

   CrPro.Kill      !Lock totally lost here

    … code NOT proctected ..

CrPro.Kill         !pointless, don't own it now

mtsynchronization_objects.htm.txt · Last modified: 2021/04/15 15:57 by 127.0.0.1