Understanding Harbour MultiThread
Understanding Harbour MultiThread
Harbour supports also SYNC methods and SYNC class method implementing
original xBase++ behavior.
In tests/mt you will find few simple test programs for MT mode.
* harbour/source/pp/ppcore.c
! fixed one typo which could cause memory leak and even GPF
* harbour/common.mak
* harbour/source/vm/Makefile
* harbour/source/rtl/Makefile
- harbour/source/rtl/set.c
+ harbour/source/vm/set.c
* harbour/include/hbstack.h
* harbour/source/vm/estack.c
* moved from RTL to HVM
* eliminated hb_set global structure
* moved set structure to HVM stack
+ added internal function hb_setClone() which is used to create
copy of SET structure for child threads
* hidden HB_SET_STRUCT declaration - 3-rd part code must not access it
directly. Dedicated hb_set*() functions should be used instead.
+ added new function:
BOOL hb_setSetItem( HB_set_enum set_specifier, PHB_ITEM pItem )
which allows the changing of some SETs using 3-rd party code.
TODO: not all SETs can be changed yet - if anybody has a little time,
then please add code for the missing ones.
* harbour/include/set.ch
* harbour/include/hbset.h
+ added _SET_CODEPAGE which works like _SET_LANGUAGE giving common
interface
* harbour/include/hbsetup.h
+ added HB_CODEPAGE_DEFAULT which works like HB_LANG_DEFAULT
* harbour/source/vm/hvm.c
! fixed builds which uses non EN lang or code page modules
by forcing linking the chosen ones
* harbour/include/hbstack.h
* harbour/source/vm/estack.c
* harbour/include/hbapicdp.h
* harbour/source/rtl/cdpapi.c
- removed global code page variable: hb_cdp_page and moved
code page settings to HVM stack
+ added new function hb_cdpID() which returns current code page
character ID
+ added new functions hb_vmCDP() and hb_vmSetCDP() to get/set
active for given thread's code page structure
* harbour/include/hbstack.h
* harbour/source/vm/estack.c
* harbour/include/hbapilng.h
* harbour/source/rtl/langapi.c
+ moved lang setting to HVM stack
+ added new functions hb_vmLang() and hb_vmSetLang() to get/set
active for given thread's language module
* harbour/include/hbvmpub.h
* harbour/include/hbstack.h
* harbour/include/hbapi.h
* harbour/source/vm/estack.c
* harbour/source/vm/dynsym.c
* harbour/source/vm/itemapi.c
* harbour/source/vm/memvars.c
* changed memvar handles for HB_HANDLE to void * which is directly
cast to PHB_ITEM - new memvar references
* changed HB_DYNS declarations for MT mode. In MT mode, HB_DYNS does
not contain area and memvar handles which are moved to thread
local HVM stack
+ added array for thread local memvar and area handles to HVM stack
% eliminated global continues array with all memvars and detached locals
% changed HB_IT_MEMVAR to use pointers to HB_ITEM directly - it resolves
synchronization problems in MT mode and should also improve the speed
and reduce memory usage. It should be easily visible in applications
which
use a lot of detached locals.
- removed hb_memvarsInit() and hb_memvarsFree() - they are not necessary
now because we no longer use array with all allocated memvars,
and detached local and private stack initialization is made
automatically
+ added internal functions hb_dynsymGetMemvar()/hb_dynsymSetMemvar()
+ added hb_memvarGetValueBySym() for debugger
* moved PRIVATE variable stack to HVM stack
* eliminated all static variables in memvars module
* harbour/include/hbstack.h
* harbour/source/vm/estack.c
* harbour/source/rtl/fserror.c
* moved IO errors to HVM stack
+ added special IO error handling which works without HVM stack
It allows the use of hb_fs*() functions without allocated stack
by 3-rd party threads.
* harbour/source/rtl/filesys.c
* moved hb_fsCurDir() to HVM stack with special handling to work
with HVM stack like IO errors
* harbour/source/rdd/workarea.c
* allocated RDD node array in bigger pieces to reduce later RT
reallocations in MT mode. If user wants to add dynamically more
then 64 RDDs then he should synchronize this operation himself.
* harbour/source/rdd/wacore.c
* moved WA list, current WA, default RDD and neteer() flag to HVM stack
* harbour/include/hbdefs.h
- removed HB_HANDLE declaration
* harbour/include/hbapi.h
- removed HB_VALUE structure - it's not longer used due to different
memvar handling
* updated hb_struMemvar to new memvar handling
* replaced hb_vmIsLocalRef() and hb_memvarsIsMemvarRef() with
hb_vmIsStackRef() which respect multiple stack and new memvar
and static structures and location in GC mark pass.
* harbour/include/hbstack.h
* harbour/source/vm/estack.c
* harbour/source/vm/hvm.c
+ added support for thread specific data located on HVM stack
Now it's possible to allocate static variables which are
local to thread. Such variables are allocated on HVM stack
and automatically destroyed. To declare new TSD (thread specific data)
variable use:
HB_TSD_NEW( <name>, <size>, <init>, <destruct> )
<name> - name of variable which holds TSD handler
<size> - size of TSD to be allocated
<init> - init function, executed when new TSD is allocated by thread
(thread access given TSD 1-st time). This function receives
void * pointer to allocated area.
<destruct> - destructor function executed when HVM stack is destroyed
f.e.:
static HB_TSD_NEW( s_scrData, sizeof( HB_SCRDATA ),
NULL, hb_xSaveRestRelease );
To initialize dynamically allocated TSD variable use:
HB_TSD_INIT( <name>, <size>, <init>, <destruct> )
Pointer to TSD can be accessed using hb_stackGetTSD( &<name> )
where <name> is name of variable which holds TSD handler, f.e.:
PHB_SCRDATA pScrData = ( PHB_SCRDATA ) hb_stackGetTSD( &s_scrData );
See source/rtl/xsavescr.c as an example
It's also possible to test if data has been already allocated for
current thread by:
hb_stackTestTSD( &<name> ) => pData
it works like hb_stackGetTSD() but return NULL if current thread data
has not been allocated yet.
* harbour/include/hbstack.h
* harbour/source/vm/estack.c
* changed hb_stack location to thread local storage in MT mode
+ added functions and macros to access/assign new HVM stack members
+ changed garbage collection mark functions to work with multiple
stacks, and thread-specific local static and memvar variables
* harbour/source/rtl/xsavescr.c
* use TSD data for screen buffer to make __XSAVESCREEN()/__XRESTSCREEN()
thread independent
* harbour/source/rtl/idle.c
* use TSD data for idle task settings and codeblocks
- removed hb_idleShutDown() - it's no longer necessary
* harbour/source/rtl/setkey.c
* use TSD data for allocated keys to make SETKEY() thread independent
* harbour/source/rtl/math.c
* moved math error handler, math error block, math error mode and
math error structure to TSD
* harbour/source/rtl/errorapi.c
* moved error handler, error block, error launch counter and DOS error
value to TSD
* harbour/source/rtl/inkey.c
* moved inkey "before" and "after" blocks to TSD
* harbour/source/rdd/hsx/hsx.c
* moved HSX handles array to TSD
* harbour/include/hbapigt.h
* harbour/source/rtl/console.c
- removed hb_setkeyInit() and hb_setkeyExit() - they are not longer
necessary; allocated resources will be freed by TSD destructor
function
* harbour/include/hbapi.h
* harbour/source/rtl/console.c
* removed hb_conXSaveRestRelease() - it's not longer necessary;
allocated resources will be freed by TSD destructor function
* harbour/source/vm/macro.c
* moved s_macroFlags to TSD
* harbour/source/rtl/accept.c
* moved accept buffer to TSD
* harbour/include/hbcomp.h
* harbour/include/hbcompdf.h
* harbour/include/hbxvm.h
* harbour/source/compiler/hbmain.c
* harbour/source/compiler/hbfix.c
* harbour/source/compiler/hbpcode.c
* harbour/source/compiler/hbdead.c
* harbour/source/compiler/complex.c
* harbour/source/compiler/genc.c
* harbour/source/compiler/gencc.c
* harbour/source/compiler/hbopt.c
* harbour/source/compiler/hblbl.c
* harbour/source/compiler/hbstripl.c
* harbour/source/compiler/harbour.y
* harbour/source/compiler/harbour.yyc
* harbour/source/compiler/harbour.yyh
* harbour/source/vm/hvm.c
+ added new PCODE HB_P_THREADSTATICS
+ added support for static variables which are local to thread:
THREAD STATIC <varname [:= <exp>], ...>
They work like normal static variables but each thread operates
on its own copy.
* added protection against possible double call to hb_xfree()
It can happen due to wrong marking of expressions as used by bison
and executing destructors after we free some code when syntax error
appears.
* harbour/source/rtl/perfuncs.prg
* harbour/source/rtl/menuto.prg
* harbour/source/rtl/getlist.prg
* harbour/source/rtl/readvar.prg
* harbour/source/rtl/text.prg
* use THREAD STATIC variables to make above code MT safe
* harbour/include/hbgtcore.h
* harbour/source/rtl/hbgtcore.c
+ added hb_gt_BaseFree() which will release current GT pointer
locked by hb_gt_Base() function. This function will be used
to optionally automate GT access synchronization when threads
share the same GT.
* harbour/source/rtl/gtapi.c
* harbour/source/rtl/inkeyapi.c
* harbour/source/rtl/mouseapi.c
* harbour/contrib/hbct/ctwin.c
* free GT pointer by hb_gt_BaseFree()
TODO: CTWIN is not MT safe yet - it will be updated together
with core GT when we add multi window interface for
thread with own console window.
* harbour/bin/hb-func.sh
* harbour/config/linux/gcc.cf
+ added rt lib to Linux builds
* harbour/bin/postinst.sh
* create MT safe version of FM stat library: fmmt
* harbour/bin/pack_src.sh
+ added support for ZIP packing
* harbour/include/hbapi.h
* harbour/include/hbvm.h
* harbour/source/vm/hvm.c
+ added hb_vmThreadInit()/hb_vmThreadQuit() functions - they initialize
HVM for calling thread so it can execute .prg code and call HVM
functions. They can be used by 3-rd party code threads.
+ added hb_vmUnlock()/hb_vmLock() functions which informs that
thread will not operate on HVM structures for some time allowing
to execute single thread only processes like GC.
+ added hb_vmThreadQuitRequest() which sends stop request to given
thread
+ added hb_vmWaitForThreads() which stops main thread execution waiting
for other threads
+ added hb_vmSuspendThreads() and hb_vmResumeThreads() used by GC
to stop all HVM threads before mark/swap scan
+ added linked list of HVM stacks
+ added hb_vmTerminateThreads() used by main HVM thread in QUIT state
* moved EXIT procedures execution from QUIT request to HVM QUIT state
in MT mode. It may effects some non structural code which tries to
access private variables in EXIT functions but it's much cleaner
and understandable for user. Please remember that we guarantee
that code in BEGIN SEQUENCE is _always_ executed even after
HVM QUIT request just like destructs. Personally I think that we
should move EXIT procedures execution also in ST mode.
* changed startup and cleanup code for new internal structures
* changes startup and cleanup code for MT mode
% removed some redundant HB_ITEM type settings
! eliminated non MT safe code which was using reference counters
without protection
* harbour/common.mak
* harbour/source/vm/Makefile
+ harbour/include/hbthread.h
+ harbour/source/vm/thread.c
+ added C level functions to manage threads and synchronization objects.
See hbthread.h for detail description. They are based on PTHREAD API
and PTHREAD documentation can be used as reference. I intentionally
keep this list small for easier multiplatform porting.
Now they have been implemented for PTHREADS (POSIX threads supported by
many different OSes), MS-Win32/64 and OS2. The OS2 version is not tested
at all. I do not even know if it can be compiled so please make tests.
I used Internet resources and some part of xHarbour code as
documentation
for OS2 MT API. It should be quite easy to add other platforms if
necessary.
Harbour core code needs non recursive mutexes, conditional variables and
TLS for one pointer. If a platform does not support conditional
variables
(f.e. MS-Win or OS2) then they can be emulated using multistate
semaphores.
+ added .prg functions to manage threads and synchronization objects:
hb_threadStart( <@sStart()> | <bStart> [, <params,...> ] ) -> <pThID>
hb_threadJoin( <pThID> [, @<xRetCode> ] ) -> <lOK>
hb_threadDetach( <pThID> ) -> <lOK>
hb_threadQuitRequest( <pThID> ) -> <lOK>
hb_threadWaitForAll() -> NIL
hb_mutexCreate() -> <pMtx>
hb_mutexLock( <pMtx> [, <nTimeOut> ] ) -> <lLocked>
hb_mutexUnlock( <pMtx> ) -> <lOK>
hb_mutexNotify( <pMtx> [, <xVal>] ) -> NIL
hb_mutexNotifyAll( <pMtx> [, <xVal>] ) -> NIL
hb_mutexSubscribe( <pMtx>, [ <nTimeOut> ] [, @<xSubscribed> ] ) ->
<lSubscribed>
hb_mutexSubscribeNow( <pMtx>, [ <nTimeOut> ] [, @<xSubscribed> ] ) ->
<lSubscribed>
The function list should give results similar to xHarbour API but they
are not exactly
the same, and except for hb_mutex*() functions, should replicate
xHarbour behavior.
+ harbour/source/vm/vmmt
+ harbour/source/vm/vmmt/Makefile
+ added hbvmmt library to GNU make builds.
Non GNU make builds should be updated.
* harbour/contrib/hbct/pos1.c
* harbour/contrib/gtwvg/gtwvg.c
* harbour/contrib/rddads/ads1.c
* harbour/contrib/hbmisc/spd.c
* harbour/contrib/hbbmcdx/bmdbfcdx.c
* harbour/contrib/examples/rdddbt/dbfdbt1.c
* harbour/source/vm/runner.c
* harbour/source/vm/itemapi.c
* harbour/source/vm/hvm.c
* harbour/source/rtl/console.c
* harbour/source/rtl/strcase.c
* harbour/source/rtl/spfiles.c
* harbour/source/rtl/defpath.c
* harbour/source/rtl/hbgtcore.c
* harbour/source/rtl/dateshb.c
* harbour/source/rtl/mlcfunc.c
* harbour/source/rtl/fstemp.c
* harbour/source/rtl/is.c
* harbour/source/rtl/setcolor.c
* harbour/source/rtl/errorint.c
* harbour/source/rtl/transfrm.c
* harbour/source/rtl/dates.c
* harbour/source/rtl/filesys.c
* harbour/source/rtl/gtdos/gtdos.c
* harbour/source/rtl/gtwin/gtwin.c
* harbour/source/rtl/gtwvt/gtwvt.c
* harbour/source/rtl/gtxwc/gtxwc.c
* harbour/source/rtl/gttrm/gttrm.c
* harbour/source/rtl/gtpca/gtpca.c
* harbour/source/rtl/gtcgi/gtcgi.c
* harbour/source/rtl/gtcrs/gtcrs.c
* harbour/source/rtl/gtstd/gtstd.c
* harbour/source/rtl/gtsln/gtsln.c
* harbour/source/rtl/gtsln/gtsln.h
* harbour/source/rdd/dbf1.c
* harbour/source/rdd/sdf1.c
* harbour/source/rdd/delim1.c
* harbour/source/rdd/dbcmd.c
* harbour/source/rdd/hbdbsort.c
* harbour/source/rdd/workarea.c
* harbour/source/rdd/dbffpt/dbffpt1.c
* harbour/source/rdd/dbfcdx/dbfcdx1.c
* harbour/source/rdd/dbfntx/dbfntx1.c
* harbour/source/rdd/hsx/hsx.c
* harbour/source/rdd/hbsix/sxfname.c
* use API functions instead of direct accessing to hb_cdp_page or hb_set
* harbour/source/rtl/fstemp.c
* harbour/source/rtl/fssize.c
* harbour/source/rtl/hbffind.c
* harbour/source/rtl/filesys.c
* encapsulate potentially slow IO operation inside
hb_vmUnlock()/hb_vmLock() calls to allow other thread GC
activation
* harbour/contrib/hbnf/fttext.c
! fixed casting
* harbour/contrib/gtwvg/gtwvg.h
- removed #include <comctl32.h> - my MinGW and MinGW/CE instalations do
not have them. If it exists in some newer ones then it has to be
covered by #if version checking.
* harbour/source/vm/dynsym.c
- removed hb_dynsymLog() and hb_dynsymMemvarHandle()
* modified code to be MT safe and improved speed of some operations
* added MUEXT protection for global dynamic table access
* harbour/include/hbapi.h
* harbour/source/vm/garbage.c
* changed to work with MT HVM
* changed to work with new memvar structures and thread local static and
memvar variables
* added MUEXT protection for linked block lists
+ added parameter to hb_gcCollectAll() which will force GC activation
in MT mode by temporary suspending all executed threads.
+ added logical parameter to HB_GCALL() functions which is passed to
hb_gcCollectAll()
* harbour/source/vm/fm.c
* added MUEXT protection for FM statistic module
* added MT protection for reference counters. For platforms
which support atomic incrmenetation/decrementation (f.e.
Interlocked*() functions in MS-Win), such operations are
used. For others, it is MUTEX protection. It gives MT safe
readonly access for HVM complex variables without user
synchronization. The MUTEX protection can cause some speed
overhead so it's good to define MT safe version of
HB_ATOM_INC()/HB_ATOM_DEC() in hbthread.h if given platform
has them. Now they are defined only for Windows. For other
platforms, we can define them in the future in assembler for some
most popular CPUs.
* harbour/source/vm/classes.c
* changed class definition array. Now it keeps pointers to class
structures.
* In MT mode allocated at HVM startup big enough array for class
definitions to avoid later RT reallocations. It effectively eliminates
MUTEX synchronization for class structure access.
* protect by MUTEX code for new class creation
* harbour/source/debug/dbgentry.c
* eliminated hbvmopt.h and direct accessing to HVM structures
* harbour/source/rtl/gtclip.c
* protect with MUTEX access to internal clipboard data
* harbour/source/rdd/nulsys/nulsys.c
+ added hb_rddCloseAll()
+ harbour/tests/mt
+ harbour/tests/mt/mttest01.prg
+ harbour/tests/mt/mttest02.prg
+ harbour/tests/mt/mttest03.prg
+ harbour/tests/mt/mttest04.prg
+ harbour/tests/mt/mttest05.prg
+ harbour/tests/mt/mttest06.prg
+ harbour/tests/mt/mttest07.prg
+ added some demonstration/test small MT programs written
using Harbour language. Some of them can be also compiled
by xHarbour but xHarbour does not pass any of my tests in
real multi-CPU machine so do not expect they will work
correctly.
Probably I'll make it for some most popular CPUs in the future.
In Harbour, each thread which wants to call HVM functions has to allocate
it's own HVM stack. It's done with hb_vmThreadInit(). The HVM stack is
freed
by calling hb_vmThreadQuit(). This functions can be called also by 3-rd
party threads if they want to call HVM functions or execute .prg code.
Calling HVM functions without allocated stack will cause GPF.
I moved most of static variables to HVM stack to make them thread
local. But some of them like FS errors have their own alternative
copy which is used when thread does not allocate HVM stack. It allows
to use hb_fs*() functions without HVM stack but programmer have to
know that error codes return by hb_fs*Error() functions can be
overwritten by other threads which also didn't allocated HVM stack.
To execute garbage collector scan and mark pass it's necessary to
stop other HVM threads. Otherwise the scan may give false results.
It's also possible to not stop threads but protect with mutex all
operations on GC items but it will probably cause performance reduction
and will force some other modifications. Maybe I'll implement it
in the future.
I didn't use any OS level thread KILL or CANCEL calls. All HVM threads
have to be cleanly removed without any resource leaks.
QUIT command terminate only calling thread. If main (startup) HVM
thread calls QUIT then it sends QUIT request to all existing threads.
Statements and destructors are executed ALWAYS in QUIT state.
New thread is created by:
hb_threadStart( <@sStart()> | <bStart> [, <params,...> ] ) -> <pThID>
The returned value is a pointer to internal thread structure which
can be used in JOIN or DETACH operations. Each thread should be Joined
or DETACHED to avoid resource leaks. If the programmer does not store
<pThID> or if all instances of <pThID> are destroyed then the thread is
automatically detached. I do not know of a clear method of thread detaching
in OS2. If some OS2 users know it then plase update vm/hbthread.c.
When thread terminates then all mutexes locked by this thread are
released.
Each thread uses its own memvars (PRIVATEs and PUBLICs) and work areas.
When new thread is created then it inherits from parent thread:
- code page
- language
- SETs
- default RDD
error block is initialized to default value by calling ERRORSYS()
and PUBLIC variable GetList := {} is created.
The following objects are initialized to default value:
- error block
- math error handler and math error block
- macro compiler features setting (hb_setMacro())
or move them to SETs.
We can think about inheriting them. It's also possible to add
inheriting of all visible memvars but I do not know it's a good
idea.
There are still some minor things which should be done but I'll
do them later. Current state seems to be fully functional.
The most important and still missing is our own file lock server
for RDD synchronization in POSIX systems. Kernel internally
recognizes POSIX locks by PID and file i-node - not by PID and file
handle. It means that the same file opened more then once by one
process shares locks. Because POSIX locks can be overwritten
then we do not have any synchronization between aliased workareas
or threads using the same table in *nixes. We have to make
synchronization ourselves. I'll create such lock server ASAP.
> Are there public WA for all MT and private WA for one MT?
No it's exactly the opposite situation. You can reopen all DBFs quite fast
in single function. Just pass them to child workarea in an array.
But if you do not want to make it then you will have to change your
application and add some mutexes to synchronize WA access between
threads. Further to not kill the performance you will have to group
your IO operations inside single locks. It will still not be efficient
enough when your application uses indexes because each thread
will discard index page tress for the other if they are operating
on different records so each skip will have to start with seek to
rebuild the path from index root node to current record key, etc.
You will also have to implement your own functions to lock workarea
which will keep current record and current order and after each WA
LOCK you will have to restore them and before each WA UNLOCK, you
will have to store current values which will be restored later.
Do you like it? You have to make it in xHarbour. And you cannot
easy reopen tables because WA aliases are global so each thread
will have to use new alias names. Full WA and alias separation
like in Harbour resolves this problems. Please also consider
that it's quite easy to implement automatic synchronization with
setting and saving RECNO and order number. Anyhow it will not
be useable for real life application. Just think what will
happen when two threads want to read 100 fields from different
records in the same WA on real multiprocessor machine:
=thread 1: locks WA makes GOTO 20, RDD reads record from file,
reads field from record buffer, unlocks WA
=thread 2: locks WA makes GOTO 30, RDD reads record from file,
reads field from record buffer, unlocks WA
=thread 1: locks WA makes GOTO 20, RDD reads record from file
again because thread 2 has just overloaded the
buffer with record 30 body, reads field from record
buffer, unlocks WA
=thread 1: locks WA makes GOTO 30, RDD reads record from file
again because thread 1 has just overloaded the
buffer with record 20 body, reads field from record
buffer, unlocks WA
[the same yet 98 times]
> With SQL data object is much easier you need only to pass
> one reference to the connection
* make_b32.mak
+ Now setting 'HB_BUILD_ST = no' will generate an MT build.
The default IS 'yes'
* bin/bld.bat
+ Added cw32mt.lib for BCC32 to make it work for MT Harbour.
; I hope this won't break ST Harbour builds, a quick test
proved not.
best regards,
Przemek
Hi Miguel,
> Meanwhile 1st thread prints invoices at background, 2nd thread prints in
screen
> every second how progress the process reading OrdKeyNo().
> Meanwhile
1st process sell ticket cinema. 2nd thread automatically prints
> a ticket
using one of your triggers.
This are independent jobs for which you need cloned workareas.
The shared one will not be enough.
We do not have native cloning support but you can simply
reopen table in second other threads to enAable such effect. When we have
cloned WA then instead of reopening you send WA from one thread to another.
The
rest of code will not be changed.
> I know what does it mean, but think that if you have a database in
> xml, ini or html file will be possible to share it.
The storage format is less important here. Each one can
be synced and only the cost will be different.
> Another case, if we want to use a function or object to manage one wa,
> and we call it from different threads, we needs that this wa will be
> common for all threads. Example we use tDataBase
> object to manage application configuration and work sometimes as mutex too.
Thread A Thread B
| |
PUBLIC aQueue := {}
**********************
FUNCTION Add( xValue )
RETURN AAdd( aQueue, xValue )
**************
FUNCTION Del()
LOCAL xValue
IF Len(aQueue) > 1
xValue := aQueue[1]
ADel ( aQueue, 1 )
ASize( aQueue, Len(aQueue)-1 )
ENDIF
RETURN xValue
In this example, the array aQueue is used to temporarily store arbitrary
values. The values are retrieved from the array according to the FIFO
principle (First In First Out). Function Add() adds an element to the end of
the array, while function Del() reads the first element and shrinks the
array by one element (note: this kind of data management is called a Queue).
When the functions Add() and Del() are executed in different threads, the
PUBLIC array aQueue is accessed simultaneously by multiple threads. This
leads to a critical situation in function Del() when the array has only one
element. In this case, a runtime error can occur:
Thread A Thread B
LOCAL xValue
IF Len(aQueue) > 1
Thread is Thread executes function completely
interrupted by the
operating system LOCAL xValue
IF Len(aQueue) > 1
xValue := aQueue[1]
ADel ( aQueue, 1 )
ASize( aQueue, Len(aQueue)-1 )
ENDIF
RETURN xValue
Thread resumes
xValue := aQueue[1]
Runtime error:
The operating system can interrupt a thread at any time in order to give
another thread access to the CPU. If threads A and B execute function Del()
at the same time, it is possible that thread A is interrupted immediately
after the IF statement. Thread B may then run the function to completion
before thread A is scheduled again for program execution. In this case, a
runtime error can occur because the function Del() is not completely
executed in one thread before another thread executes the same function.
The example with the above mentioned PUBLIC array aQueue must be programmed
as a user-defined class in order to safely access the array from multiple
threads:
***********
CLASS Queue // Class for managing
PROTECTED: // a queue
VAR aQueue
EXPORTED:
INLINE METHOD init
::aQueue := {} // Initialize array
RETURN self
IF Len(::aQueue) > 1
xValue := aQueue[1]
ADel ( ::aQueue, 1 )
ASize( ::aQueue, Len(::aQueue)-1 )
ENDIF
RETURN xValue
In this example, the Queue class is used for managing a dynamic array that
may be accessed and changed from multiple threads simultaneously. The array
is referenced in the instance variable :aQueue and it is accessed within the
SYNC methods :add() and :del() . The Queue object which contains the array
is globally visible. The execution of the :del() method is automatically
serialized between multiple threads:
Thread A Thread B
| |
oQueue:del() |
| oQueue:del()
<...>
ADel( ::aQueue, 1 ) thread is stopped
<...>
RETURN xValue thread resumes
| <...>
| ADel( ::aQueue, 1 )
| <...>
| RETURN xValue
| |
When thread B wants to execute the :del() method while this method is
executed by thread A, thread B is stopped because it wants to execute the
method with the same Queue object. Therefore, SYNC methods are used whenever
multiple operations must be guaranteed to be executed in one thread before
another thread executes the same operations. A SYNC method can be executed
with the same object only in one thread at any time (Note: if a class method
is declared with the SYNC attribute, its execution is serialized for all
objects of the class).
The function Signal() returns the class object of the Signal class.
Description
Signal objects are used to control program flow within one or more threads
from one other thread. To achieve this, it is necessary that one and the
same Signal object is visible in at least two threads. Therefore, Signal
objects can only be used in conjunction with Thread objects. In other words,
at least one additional Thread object must exist in an application for the
Signal class to become useful (note: the Main procedure is executed by an
implicit Thread object).
There are two methods in the Signal class: :signal() and :wait() . The
method :signal() triggers a signal, while the :wait() method causes a thread
to enter a wait state. If a thread executes the :wait() method, it waits for
the signal and suspends program execution. The thread resumes as soon as a
second thread executes the :signal() method with the same Signal object.
With the aid of Signal objects it becomes possible for the current thread to
control program flow in other threads. Each thread which executes the
:wait() method enters a wait state until another thread executes the
:signal() method.
Caution: Multiple Signal objects can be used to control program execution in
multiple threads. Since a Signal object limits program control in threads to
a wait state, so-called "dead lock" situations can occur when multiple
Signal objects are used. A dead lock is reached when thread A waits for a
signal from thread B, while, at the same time, thread B waits for a signal
from thread A.
Class methods
:new()
Creates an instance of the Signal class.
Instance variables
Attribute: EXPORTED
Methods
Example - 1
Example - 2
PROCEDURE Main
LOCAL oReadDone, oWriteDone, oDoExit, oThread, aValues
USE LargeDB
DbCreate( "Test", DbStruct() )
oThread := Thread():new()
aValues := Array( FCount() )
oReadDone := Signal():new()
oWriteDone := Signal():new()
oDoExit := Signal():new()
oThread:start( "AppendTo", "Test" , aValues , ;
oReadDone, oWriteDone, oDoExit )
oWriteDone:wait() // Wait until 2nd database
// is open
DO WHILE .T. // Transfer record to array
Aeval( aValues, {|x,i| x:=FieldGet(i) },,, .T. )
IF ! Eof()
oReadDone:signal() // 1st signal: record is read
SKIP
ELSE
oDoExit:signal() // 2nd signal: terminate thread #2
EXIT
ENDIF
oWriteDone:wait() // Wait until data is written
ENDDO // to new database
oThread:synchronize(0)
USE
RETURN
//-------------------------------------------------//
Description
The function ThreadID() is used to get the numeric ID of the thread in which
the function is called. The value is unique within a single process and may
occur multiple times in different processes.
<nWhichInfo>
#define constants from the file THREAD.CH must be used for this parameter.
They determine the type of information returned by the function. Multiple
constants can be combined with '+'.
<nThreadID>
Return
The function returns a two-dimensional array where each thread has one
sub-array containing one or more elements controlled by <nWhichInfo> . If
<nWhichInfo> is a combination of constants, the elements of the sub-array
(the array columns) appear in the sequence order of each constant (first
column in the table).
Description
Example
#include "Thread.ch"
PROCEDURE Main
LOCAL aInfo, aThread := { Thread():new(), Thread():new() }
CLS
aThread[1]:start( "Test1" )
aThread[2]:start( "Test2" )
THREADINFO_FUNCINFO + ;
THREADINFO_TOBJ )
?
? "Retrieve partial thread information"
aInfo := ThreadInfo( THREADINFO_FUNCINFO + ;
THREADINFO_TID )
?
AEval( aInfo, {|a| QOut( Var2Char(a) ) } )
RETURN
PROCEDURE Test1
DO WHILE .T.
DispOutAt( 10, 10, Time() )
Sleep(100)
ENDDO
RETURN
PROCEDURE Test2
LOCAL nCount := 0
DO WHILE .T.
DispOutAt( 12, 10, nCount ++ )
Sleep(17)
ENDDO
RETURN
// Program output
The function returns the Thread object which is currently executing program
code.
Description
<aThreads>
<nTimeOut>
<nTimeOut> specifies the maximum amount of time the current thread will wait
for the termination of other threads. The unit is 1/100th of a second. The
default value is 0 and this causes the current thread to wait infinitely
until one Thread object in <aThreads> has finished with executing code. If a
value > 0 is specified for <nTimeOut> and no thread has terminated within
this period of time, the current thread resumes progam execution.
Return
If one thread terminates within the time limit defined with <nTimeOut> , the
function returns the corresponding Thread object. Otherwise, the return
value is NIL.
Description
The function ThreadWait() causes the current thread to wait until one thread
from a number of threads has terminated. Compared to the :synchronize()
method of the Thread class, the function has the advantage that the current
thread does not need to wait for a particular thread, but rather can wait
for any thread from a number of threads. Note that the number of threads the
current thread can wait for is limited to the constant THREAD_WAIT_MAX by
the operating system.
<aThreads>
<nTimeOut>
<nTimeOut> specifies the maximum amount of time the current thread will wait
for the termination of all other threads. The unit is 1/100th of a second.
The default value is 0 and this causes the current thread to wait infinitely
until all other Thread objects are finished with executing code. If a value
> 0 is specified for <nTimeOut> and no thread has terminated within this
period of time, the current thread resumes progam execution.
Return
When all threads specified in <aThreads> have terminated, the return value
is .T. (true). If the waiting period is limited by a value greater than 0
for <nTimeOut> and not all threads have terminated when this time limit has
expired, the function returns .F. (false).
Description
The function ThreadWaitAll() causes the current thread to wait for the
termination of multiple other threads. Compared to the :synchronize() method
of the Thread class, it has the advantage that the current thread does not
need to wait for one particular Thread object but rather can wait for
multiple threads to complete execution of code.
> I know that it can fall down performance for shared wa, but will be a
> programmer election to make it public or private wa
But you have such possibilities now. All examples you are
presenting so far need only one thread access or cloned workareas. So far none
of them needs shared workareas.
In the final analysis, everything can be done by simple table reopening. You
can still use mutexes instead of RDD locks to synchronize the access.
SUBSCRIBE is the same as WAIT and NOTIFY as in SIGNAL. It was not clear to
me from the documentation Pritpal sent if xbase++ WAIT internally works like
hb_mutexSubscribe() or hb_mutexSubscribeNow(). The valid one should be used.
If you need a class which will work like in xBase++ then you can implement it
in a few lines.
We do not yet have SYNC methods at all but I implemented low level code
for them in thread.c and I have yet to update classes.c.
xHarbour have only CLASS SYNC methods but it also does not support normal
SYNC methods though it compiles code which uses them but internally convert
the function to class methods synchronization.
> it), maybe having the same support in harbour could make it easier to move
> code from xbase++/xharbour to harbour and vv.
Yes. But I'll prefer to create basic function interface which should allow
to easily write upper level code like synchronization object in xBase++.
Hello Przemek
When I was working with Xbase++ long back, probably in year 2002-03, I
exploited this feature mainly for only one table which was responsible for
some synchronization for a particular operation. Nowhere did I need this
feature otherwise.
MTs importance is gone once resources become shared between threads. MT has a
meaning only when everything in a thread is self-contained. Your
implementation of MT is PERFECT.
Just consider this scenario which Angel is emphasizing:
//------------------------------------------------//
Syntax
DbRelease( [<nWorkArea>|<cAlias>],[<bAreaBlock>] ) --> lSuccess
Parameters <nWorkArea>
<nWorkArea> is a positive integer specifying the work area whose alias name is
to be transferred into the Zero space. It defaults to the number of the
current work area.
<cAlias>
<bAreaBlock>
Return
Description
Exchanging alias names between work spaces always involves a virtual work
space, the Zero space. Function DbRelease() releases the alias name of a work
area in the current work space. It is then collected in the Zero space and
transferred to a work area of another work space when DbRequest() is called.
If the work area is linked to other work areas (SET RELATION) alias names of
all child work areas are transferred as well.
Caution
When an alias name is transferred to the Zero space, database access in the
corresponding work area is not possible. The work area is then not used and
function Used() returns .F. (false). However, the database file is still open.
Example
// Perform database operations in a separate thread
RETURN
*********************
PROCEDURE
DbOperation //
Procedure for any
LOCAL bBlock,xResult // database operation.
IF DbRequest(, , @bBlock, .T. ) // Wait for work area.
xResult :=Eval( bBlock ) // Perform operation.
DbRelease(, {|| xResult } ) // Release work area and
ENDIF // return result within
// a code block.
RETURN
************************
FUNCTION DbCount( bFor ) // Count all records
LOCAL nCount := 0 // that match a
GO TOP // condition.
DO WHILE ! Eof()
IF Eval( bFor )
nCount++
ENDIF
SKIP
ENDDO
RETURN nCount
//------------------------------------------------------------//
Syntax
Parameters
<cAlias>
<cAlias> is a character expression specifying the alias name of the work area
to be transferred from the Zero space into the current work space. If not
specified, the next alias name pending in the Zero space is transferred.
<lFreeArea>
<@bAreaBlock>
<lWait>
The optional parameter <lWait> determines whether DbRequest() waits until a
work area is found in the Zero space and can be transferred to the current
work space. It defaults to .F. (false). This means that the function does not
wait but returns immediately even if the Zero space is empty. If .T. (true) is
specified, the function waits until it detects an alias name in the Zero space
that can be transferred.
Return
Description
The transfer of alias names follows the FIFO principle (First in first out).
> I see that fwh continues calling hb_gcall() after each time a dialog
> close. It remains necessary in current svn or we have a better choice
Not yet but Harbour is ready for automatic GC activation which I'll enable
soon. Anyhow it may effect 3-rd party code which keeps not
attached GC blocks. With current API it can be done quite easy even by
mistake.
This problem can be resolved by changing the semantic and/or names of few
hb_gc*() functions but it may force 3-rd party code modification if it uses
them.
In general hb_gcAlloc() should always return locked block. Storing this block
in HB_ITEM (hb_retPtrGC(), hb_storptrGC(), hb_itemPutPtrGC()) should
automatically unlock it by calling hb_gcAttach(). hb_gcLock() and
hb_gcUnlock() will be less important and in practice not longer necessary so
probably can be dropped from public API.
I do not want to make too many modifications simultaneously in order to be
able to easy rectify the problems reported by users. But if everything works
OK with new MT code then I'll add automatic GC activation.
Few Question
Hi Mindaugas,
> 2) We use HB_ATOM_INC for strings usage counters. Can HB_ITEMs used by
> a few threads, or each thread uses its own HB_ITEMs? For example, can
> we pass parameters to hb_threadStart() by reference? If HB_ITEMs can
> be shared among threads, then we need synchronization to access
> HB_ITEMs value (not reference counter only).
Variables can be fully shared between threads. You can pass them by reference,
detach and move between threads directly or indirectly f.e. by codeblocks.
There is only one exception which is not safe even in single thread mode just
like in Clipper. Storing local references in arrays ( { @localVar } ) does not
automatically detach them from HVM stack.
It allows to create code which may corrupt HVM stack, f.e you can create
references to unused part of stack...
Please only remember that HVM synchronizes only read only access to variables.
If you want to modify variables which can be accessed simultaneously by
different threads then you should add your own protection code. Now you can
uses mutexes (they are also conditional variables and message queues) for this
job. I'll add support for object sync methods soon. You can think about this
mechanism just like about automatic instance protection. Each object will
have it's own critical section which is gate for each methods
declared with SYNC attribute.
> to implement synchronous (blocking) IO, let say,wait for some data on
> COM port, instead of asynchronous IO (processing some notification
> about data arival, etc). How can this blocking IO thread can be terminated?
> Is it OK to kill a thread if I do not want to listen port any more and
> want to exit application? We do not have function to terminate thread
> among hb_thread*().
When main thread leaves HVM (by calling QUIT or returning from MAIN procedure
then QUIT request is sent to all HVM threads and main thread waits for all
other HVM thread termination. Anyhow it does not break any blocking IO
operations. It has to be implemented in the thread code.
For our own code, f.e. using hb_inet*() functions I hope we will add
respecting HVM signals ASAP. But 3-r dparty code will have to be adapted to
respect it.
>From your own code you can use hb_threadQuitRequest( <pThID> ) which sends
thread request to given thread with blocking the caller and
hb_threadTerminateAll() which sends QUIT request to all threads and waits for
their termination blocking the caller. But as I said above sending QUIT
request will have immediate effect only on .prg code or .c code which uses
hb_vmLock()/hb_vmUnlock() and/or hb_vmRequestQuery() and will not cancel/wake
up threads waiting for blocking IO operations if the code was not adapted for
such situations.
> Sorry, for dumb questions. Just MT is new think for me. And I would
> like to put some communication code into synchronous mode, to avoid
> confusing protocol state machine.
There are not dumb questions but very good ones. They also greatly illustrate
the problems which we have to resolve and which have to be resolved by user.
BTW if you need some working solution now, then you can use simple pooling
with some timeout f.e. few seconds to leave hbinet.c code for a while and your
thread will be terminated inside .prg code immediately.
/*
Example multiThreads index.
One thread by table , and one thread by index.
2010 Rafa Carmona
Thread Main
|---------> Thhread child table for test.dbf
| |----> Thread child index fname
| |
| |----->Thread child index fcode
|
|---------> Thhread child table for test2.dbf
|----->Thread child index fname2
*/
#include "hbthread.ch"
setmode( 25,80 )
cls
? "finish"
return
use ( cFile )
cAlias := alias()
hb_dbDetach( cAlias ) // Libero el alias
RETURN NIL
return