|Synchronizing Execution and Resolving Contention||Multi-threading Compiler Directives|
When you create a multi-threaded application you need to consider how each program in the application should operate: as a standard COBOL program, as a serial program, or as a reentrant program. Of the three types of program, programs compiled as REENTRANT(1) programs need most attention.
You also to need to consider how to:
You should also be aware of common problems with multi-threaded programming .
The run-time system for multi-threaded applications needs to handle both multi-threaded and single-threaded applications. As a result, the multi-threaded run-time system incurs many of the synchronization costs that a standard multi-threaded COBOL application would. Further, most of these costs are incurred regardless of whether the application is multi-threaded or single-threaded. Because of this, and in order to allow single-threaded applications to run at maximum speed, Micro Focus provides two separate run-time systems: a multi-threaded run-time system, which supports all applications, and a single-threaded run-time system which supports only single-threaded applications. You can choose between these run-time systems either when you run your application when you link your application.
The files you need to ship with an application will depend in some cases on whether your application has been linked to the single-threaded or multi-threaded run-time system. See the help topic Determining the Run-time Files to Ship for details.
Not every program can be compiled as reentrant. Use of some COBOL features will preclude reentrant compilation; most of these COBOL features are obsolete or archaic in ANSI Standard COBOL and their use should be discontinued. Programs that use the following features cannot be compiled with the REENTRANT directive:
If you are creating a reentrant program, there are also other restrictions you should be aware of:
The following performance and resource penalties are incurred in reentrant COBOL programs within a multi-threaded application:
Call-by-name library routines provide a programming interface through which you can implement and control multi-threading in your applications. Library routines are provided for:
The library routines for controlling threads can be used to implement threads that wait for another thread to terminate and then pick up a return value.
Any thread created using these library routines must:
A thread might not have been created by the thread-control routines. However, such a thread might still be known to the run-time system by the thread's use of run-time system services. Such a thread can be accessed via the thread-control library routines; it can use:
In a mixed-language environment, any thread known to the run-time system from its use of the run-time system, but not created by the run-time system or any of the thread control routines, must do one of the following:
cobthreadtidy()before terminating. This call informs the run-time system that the thread no longer needs its services and allows theit to cleanup its thread-state data.
cobtidy()to indicate that the COBOL run-time system will not be needed again in this application run.
cobexit()to terminate the run-unit.
An application can check that the run-time system used by a program supports the thread-control routines by calling either CBL_GET_OS_INFO or CBL_THREAD_SELF.
Example - Checking Which Run-time System a Program Supports
NetExpress provides four types of synchronization object: monitors, semaphores, mutexes, and events. Control of these synchronization objects is provided by COBOL syntax, and also by call-by-name library routines.
You can create dynamic data whose lifetime is that of the creating thread, or selective or external thread-local data, using the run-time library routine CBL_ALLOC_THREAD_MEM and the library routines CBL_TSTORE_n.
The initialization of multi-threaded applications needs some care. First, as good programming practice, the application should determine if it is running with a run-time system that supports multi-threading. See the following example.
Example - Checking the Run-time System
This example makes use of the run-time library routine CBL_THREAD_SELF. While many facilities of multi-threading are accessible through the COBOL syntax, all facilities are also accessible through COBOL run-time library routines. The run-time library routines also offer advanced multi-threading programming capabilities that are unavailable through the COBOL syntax.
After the application has determined that the run-time system supports multi-threading, all synchronization primitives should be be initialized using the appropriate OPEN syntax. It is easiest to do this as part of the main program, before any other threads are created within the application. However, if this not possible, either because of the way the application is designed, or because of modularity or mixed-language considerations, Micro Focus have provided a single pre-initialized mutex per program. This mutex is accessed using the CBL_THREAD_PROG_LOCK and CBL_THREAD_PROG_UNLOCK run-time library routines. Using these routines, it is possible to ensure that any handles for program -local synchronization primitives are initialized once (and only once) during the execution of the application.
Example - Thread Locking Using Library Routines
The following code illustrates a sample use of these routines for program initialization while running in multi-threaded mode:
Note the double checking of the
After the initialization is completed, and while the initializing thread
still owns the lock, the
The flag that specifies whether initialization has occurred should as simple as possible (that is, a single byte data item).
The multi-threaded run-time system supports threads created by the run-time system itself, via the START verb or the CBL_THREAD_n library routines, and also other-language threads created directly by the operating system. Since the COBOL syntax and the CBL_THREAD_n routines are portable, these are the preferred methods of thread manipulation. Furthermore, the CBL_THREAD_n routines can also be called from programs written in other languages, allowing any multi-threaded application to take full advantage of the advanced facilities provided by the run-time system.
In general, threads are identified to the run-time system by thread handles. Thread handles are provided to:
The thread handle uniquely identifies the thread to the various multi-threading COBOL verbs or to CBL_THREAD_n routines. With reasonable care, thread handles can be used by any of the active threads in the run-unit, not just the creator of a thread. It is important to remember that the lifetime of a thread and that of its thread handle might not be the same. Depending on how a thread was created and what operations have been done on it, a thread handle could still be valid even if the thread has terminated. In order to enable the system to recover the resources associated with the handle, it is up to the user to explicitly detach the handle for a thread.
A handle can become detached from its thread in one of several ways. When a thread is created with the START verb or CBL_THREAD_CREATE routine, its handle is, by default, created as detached. When another-language thread becomes known to the run-time system, its handle is automatically and always created as detached. A non-detached handle can be detached with a call to CBL_THREAD_DETACH. In any case, a detached handle must always be used with care, as when the thread terminates the handle immediately becomes invalid. If a thread with a non-detached handle has already terminated, then a call to CBL_THREAD_DETACH with that thread handle will immediately cause the handle to become invalid.
A non-detached handle is useful for retrieval of return values (via the WAIT verb or CBL_THREAD_WAIT routine), and for inspection of thread identification data (via the CBL_THREAD_IDDATA_GET routine) after a thread has potentially terminated. The START verb provides a way of identifying a newly created thread by returning a non-detached thread handle. The CBL_THREAD_CREATE routine provides a flag to indicate that the thread should be created and a non-detached handle returned.
Threads are either created by the:
Threads created by the first two methods are called COBOL threads. Threads created directly by the operating system are called other-language threads.
As the creation and manipulation of COBOL threads is portable, and the preferred method, we will only deal with them here.
The starting point for a thread must be either a non-nested Program-Id, a COBOL entry-point or a an externally visible routine written in another language, such as C. It must not be a nested program, a section or paragraph name.
The name of the starting point can be specified as a text string. The overhead in using a text string to find the entry point is equivalent to resolving the verb CALL identifier. It is possible to avoid this overhead by using a procedure pointer as the object of a START verb.
The starting point for every created thread is provided with only one parameter. If the BY CONTENT phrase is used in passing this parameter, a copy of it is made by the system before the thread is created, leaving the calling thread free to modify the original parameter upon return from the START verb.
Creation of a thread with a non-detached handle (via the IDENTIFIED BY clause) enables the return value to be obtained later by the WAIT verb. After the WAIT verb has completed the specified thread handle becomes invalid and all resources associated with the subject thread are released.
A STOP RUN RETURNING statement in the created thread does not end the run-unit; it simply provides a return value and terminates the thread. It is equivalent to:
call 'CBL_THREAD_EXIT' using by value address of thread-parm.
STOP RUN in other-language threads or the main thread of a run-unit not created by CBL_THREAD_CREATE will wait for all active COBOL threads to finish and then terminate the run-unit.
Return values from threads are always pointers. This allows you to return both simple and complex data structures.
A thread can terminate itself with STOP RUN or a call to CBL_THREAD_EXIT. A thread also terminates normally when the starting point program exits via EXIT PROGRAM or GOBACK. It is also sometimes useful for another thread to cause a COBOL thread to be terminated. The CBL_THREAD_KILL routine achieves this.
Example - Creating and Terminating a Thread
This application simply creates a thread with the starting point CREATED. The thread displays its parameter and returns the address of that parameter for use in the parent thread. The STOP RUN RETURNING in the CREATED thread does not end the run-unit, instead it simply provides a return value and terminates the thread. It is equivalent to:
call 'CBL_THREAD_EXIT' using by value address of thread-parm.
Threads created with CBL_THREAD_CREATE can be canceled with the CBL_THREAD_KILL routine. You cannot use CBL_THREAD_KILL to kill threads created in other ways. This call causes immediate, abnormal termination of a COBOL thread but, in general, it should not be used as part of general application thread control. The main reason for this is that some user and system synchronization resources will not be properly unlocked and/or freed when the thread terminates. This can affect the synchronization within the user application and the run-time system, which can cause serious problems with the application.
CBL_THREAD_KILL can reasonably be used within a critical error handler in the main thread. In this error handler, thread handles can be acquired by way of the CBL_THREAD_LIST_n routines, all threads can be canceled and the application can then exit via STOP RUN. This is less dangerous than the random use of CBL_THREAD_KILL as it minimizes the chance of needing a locked synchronization primitive but there is still a possibility of file corruption or deadlock during run-unit termination.
In any case, most applications should avoid using CBL_THREAD_KILL. This can be achieved by creating thread identification data (through the CBL_THREAD_IDDATA_ALLOC routine) that has a terminate flag in it. That data can then be polled in each thread at a level where no locks are held. If the terminate flag is set, then the polling thread can exit gracefully.
Example - Creating Thread Identification Data and Terminate Flags
This rather lengthy example doesn't actually do anything but establish handshaking for thread and application termination. Before going into any discussion on it, it is worth noting that this kind of handshaking could have been accomplished more easily by passing the main thread's handle into the child as a parameter. This would have avoided the need to rely on identification data or to step through the thread list.
First, notice the two different ways of creating thread identification data. The first way, in the parent thread, creates the data uninitialized, retrieves the iddata pointer and then initializes the area of memory that was allocated. The second, in the child, provides initialization data to eliminate any possible execution window between iddata allocation and iddata initialization by the application. The method used in your application will depend on what level of contention you expect on identification data.
Also, notice the loop in the parent that waits for the child to create its thread identification data and the loop in the child that waits for the parent to flag termination. The call to CBL_THREAD_YIELD prevents these loops from being hard busy waits, but would have been better coded using an event or condition synchronization object or using CBL_THREAD_SUSPEND and CBL_THREAD_RESUME.
Finally, notice the use of the CBL_THREAD_LIST_ API. This API allows a thread to step through all threads known to the RTS, obtaining the thread handle, thread state and identification data pointer. In this example we relied only on the handle and identification data pointer but the state information can also be useful, as it lets the calling thread know if the subject thread is detached, suspended or an other-language thread.
It is important to realize that whenever the CBL_THREAD_LIST API is used, the using thread has limitations on further CBL_THREAD_ calls and all other threads are completely locked out from using any of these calls as well as quite a few of the other RTS calls. For this reason, it makes sense to keep the amount of code executed while stepping the list to a minimum and certainly avoiding file or user I/O while the list is locked. These restrictions are removed only after the stepping is terminated by a call to CBL_THREAD_LIST_END
In many multi-threaded applications it is not uncommon for a thread to 'run out of work' but not need to terminate just yet. In a client-server architecture, for example, the main service thread could be polling an input queue for requests and if it finds none then it makes sense to allow another process or thread to have the CPU before polling the input queue again. This is simply done by calling CBL_THREAD_YIELD which yields the processor to some other thread in the application (or some other thread in another process, depending on the operating system).
Another possibility is that a thread needs to yield the CPU indefinitely and obtain it only when some sort of event has definitely happened. CBL_THREAD_SUSPEND allows suspension of the calling thread until some other thread does a CBL_THREAD_RESUME using the thread handle for the suspended thread. A thread can call CBL_THREAD_RESUME one or more times before the targeted thread suspends itself and, in this case, the call to CBL_THREAD_SUSPEND returns to the calling thread immediately and does not give up ownership of the CPU. This operation is very similar to that of a counting semaphore; in fact, the Producer-Consumer problem can be solved using only the CBL_THREAD_SUSPEND and CBL_THREAD_RESUME routines.
It is sometimes useful for a thread to distinguish itself from any other created threads in an application. For example, if two threads in a four thread application establish a producer-consumer relationship, it can be useful for the two cooperating threads to find out what each other's thread handles are. Once these handles are obtained, all synchronization can be done using only the CBL_THREAD_SUSPEND and CBL_THREAD_RESUME calls. If each thread in the application creates a name for itself (and the name relates to its functionality) and associates the name with its thread handle, then the cooperating threads can scan the thread name list and find each other's handle.
Another possible use for associating globally accessible data to each thread and its handle is to hold a termination flag, obviating the possible need to use CBL_THREAD_KILL. Each thread in the application can poll its termination flag to check for a termination request. The terminating thread can then make sure no locks are held and no synchronization actions are required before terminating normally.
Globally accessible data for a thread is associated with the thread handle by the CBL_THREAD_IDDATA_ALLOC routine executed within the thread. This data is retrieved by the CBL_THREAD_IDDATA_GET routine, if the thread handle is already known, or by the CBL_THREAD_LIST_n routines if the thread handle is not yet known.
Example - Associating Globally Accessible Data With a Thread Handle
The following example shows how to associate globally accessible data with the thread handle by using the CBL_THREAD_IDDATA_ALLOC call executed within the thread. The data is retrieved by the CBL_THREAD_IDDATA_GET call, if the thread handle is already known, or by the CBL_THREAD_LIST_n routines if the thread handle is not yet known.
**************************** MAINPROG.CBL **************************** identification division. program-id. mainprog. Data Division. Local-Storage Section. 01 iddata-ptr usage pointer. 01 sub-iddata-ptr usage pointer. 01 sub-handle usage thread-pointer. Linkage Section. 01 iddata-record. 05 iddata-name pic x(20). 05 iddata-term pic x comp-x value 0. Procedure Division. *> *> Establish identification data - don't provide *> initialization data when it is allocated, instead *> initialize it after the pointer is retrieved. *> call 'CBL_THREAD_IDDATA_ALLOC' using by value zero length of iddata-record call 'CBL_THREAD_IDDATA_GET' using iddata-ptr by value 0 set address of iddata-record to iddata-ptr move 'main' to iddata-name *> *> Create sub-thread *> start 'SUBPROG ' identified by sub-handle *> *> Wait until child creates its iddata and then flag *> termination *> set sub-iddata-ptr to NULL perform until 1 = 0 call 'CBL_THREAD_IDDATA_GET' using sub-iddata-ptr by value sub-handle if sub-iddata-ptr not = null exit perform end-if call 'CBL_THREAD_YIELD' end-perform set address of iddata-record to sub-iddata-ptr move 1 to iddata-term *> *> Wait until the child resumes this thread *> call 'CBL_THREAD_SUSPEND' using by value 0 display 'All synchronization is complete on RTS termination' wait for sub-handle *> clear up thread's resources stop run. end program mainprog. ***************************** SUBPROG.CBL **************************** identification division. program-id. subprog. Data Division. Working-Storage Section. 01 sub-iddata. 05 sub-name pic x(20) value 'sub'. 05 sub-term pic x comp-x value 0. Local-Storage Section. 01 iddata-ptr usage pointer. 01 thread-handle usage pointer. 01 thread-state pic x(4) comp-x. 01 parent-handle usage pointer. Linkage Section. 01 iddata-record. 05 iddata-name pic x(20). 05 iddata-term pic x comp-x value 0. Procedure Division. *> *> Establish identification data - provide *> initialization data call 'CBL_THREAD_IDDATA_ALLOC' using sub-iddata by value length of sub-iddata *> *> Find our parent thread and resume him *> call 'CBL_THREAD_LIST_START' using thread-handle thread-state iddata-ptr set parent-handle to NULL perform until thread-handle = null or return-code not = 0 if iddata-ptr not = null set address of iddata-record to iddata-ptr if iddata-name = 'main' set parent-handle to thread-handle exit perform end-if end-if call 'CBL_THREAD_LIST_NEXT' using thread-handle thread-state iddata-ptr end-perform call 'CBL_THREAD_LIST_END' if parent-handle = NULL display 'synchronization error' stop run end-if call 'CBL_THREAD_RESUME' using by value parent-handle call 'CBL_THREAD_IDDATA_GET' using iddata-ptr by value 0 set address of iddata-record to iddata-ptr perform until iddata-term = 1 call 'CBL_THREAD_YIELD' end-perform exit program. end program subprog.
This example establishes handshaking for thread and application termination. Note that this kind of handshaking could have been accomplished more easily by passing the handle of the main thread into the child as a parameter. This would have avoided the need to rely on identification data or to step through the thread list.
The example shows two different ways of creating thread identification data. The first , in the parent thread, creates the data uninitialized, retrieves the identification data pointer and then initializes the area of memory that was allocated. The second, in the child, provides initialization data to eliminate any possible execution window between identification data allocation and identification data initialization by the application. The method used in your application will depend on what level of contention you expect on identification data.
Note the loop in the parent that waits for the child to create its thread identification data and the loop in the child that waits for the parent to flag termination. The call to CBL_THREAD_YIELD prevents these loops from being hard busy waits, but would have been better coded using an event or condition synchronization object or using CBL_THREAD_SUSPEND and CBL_THREAD_RESUME.
Finally, note the use of the CBL_THREAD_LIST_n routines. These allow a thread to step through all threads known to the RTS, obtaining the thread handle, thread state and identification data pointer. In this example we relied only on the handle and identification data pointer but the state information can also be useful, as it lets the calling thread know if the subject thread is detached, suspended or an other-language thread.
It is important to realize that whenever the CBL_THREAD_LIST_n routines are used, the thread using them has limitations on further CBL_THREAD_n calls, and all other threads are completely locked out from using any of these calls as well as some of the other run-time system calls. For this reason, it makes sense to keep the amount of code executed while stepping the list to a minimum, and avoid file or user I/O while the list is locked. These restrictions are removed only after stepping the list is terminated by a call to CBL_THREAD_LIST_END.
In a mixed language environment, threads created directly by operating system facilities and not by the START verb or the CBL_THREAD_CREATE routine have several restrictions and requirements on them:
cobtidy()when the thread or the application, respectively, no longer needs COBOL run-time system services. This allows the run-time system to free memory and clean up thread-state information.
cobexit()or the COBOL STOP RUN statement will cause the calling thread to be treated as the 'main' thread. As a result, the run-time system will wait for all CBL_THREAD_CREATE threads to terminate before any run-time system cleanup processing is completed.
Note: All the multi-threading functions supported by the run-time system detect handles created by threads in other languages, and reports invalid use of them. It is, however, entirely up to the application to ensure that run-time system resources are freed for any thread that has executed any COBOL or run-time system code in the course of its existence (as detailed above).
To create optimized and efficient programs, note the following:
The Compiler directives available for multi-threaded applications can affect the performance of your multi-threaded and single-threaded applications.
Copyright © 1998 Micro Focus Limited. All rights reserved.
This document and the proprietary marks and names used herein are protected by international law.
|Synchronizing Execution and Resolving Contention||Multi-threading Compiler Directives|