Navigation: Advanced Topics > Thread Model Documentation > Multi-Threading Programming >====== Synchronization Objects ====== | |
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.
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 &= NewMutex('MyApplicationLimiterMutex',,LastErr)
IF Limiter &= NULL
MESSAGE ('ERROR: Mutex can not be created ' & 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 ' & 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 &= 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 &ISemaphore
GlobalStrings QUEUE,PRE(Q)
Data STRING(50)
END
AppFrame APPLICATION('Reader/Writer'),AT(,,400,240),SYSTEM,MAX,RESIZE
MENUBAR
MENU('&File'),USE(?FileMenu)
ITEM('E&xit'),USE(?Exit),STD(STD:Close)
END
MENU('&Launch'),USE(?LaunchMenu)
ITEM('Reader'),USE(?LaunchReader)
ITEM('Writer'),USE(?LaunchWriter)
END
END
END
CODE
LimitReaders &= NewSemaphore(1)
SHARE(LogFile)
IF ERRORCODE() = NoFileErr
CREATE(LogFile)
SHARE(LogFile)
END
IF ERRORCODE()
STOP('Log File could not be opened. Error: ' & 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. ' &|
'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 ' & 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 &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 &= NewReaderWriterLock()
GlobalVars.Destruct PROCEDURE
CODE
SELF.AccessToGlobals.Kill()
GlobalVars.GetBackground PROCEDURE()
ret SHORT,AUTO
Reader &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 &= SELF.AccessToGlobals.Reader()
Reader.Wait()
ret = SELF.Background
Reader.Release()
RETURN ret
GlobalVars.PutBackground PROCEDURE(SHORT newVal)
Writer &ISyncObject
CODE
Writer &= SELF.AccessToGlobals.Writer()
Writer.Wait()
SELF.Background = newVal
Writer.Release()
GlobalVars.GetTextSize PROCEDURE()
ret SHORT,AUTO
Reader &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 &= SELF.AccessToGlobals.Reader()
Reader.Wait()
ret = SELF.TextSize
Reader.Release()
RETURN ret
GlobalVars.PutTextSize PROCEDURE(SHORT newVal)
Writer &ISyncObject
CODE
Writer &= 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 &ICRITICALSECTION
CODE
Locker &= NewCriticalSection()
ASSERT(~Locker &= 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 &ICRITICALSECTION
CODE
Locker &= NewCriticalSection()
ASSERT(~Locker &= 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.
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 &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