PreviousSynchronizing Execution and Resolving Contention Multi-threading Compiler DirectivesNext

Chapter 3: Writing Multi-threaded Applications

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 .

3.1 Run-time System for Multi-threaded Applications

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. Furthermore, most of these costs are incurred regardless of whether the application is multi-threaded or single-threaded. Because of this, and in order to enable 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 or 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.

3.2 Considerations When Creating Reentrant Programs

Not every program can be compiled as reentrant. Use of some COBOL features will preclude compilation of a program as reentrant; 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:

3.3 Multi-threading Library Routines

Call-by-name library routines provide a programming interface through which you can implement and control multi-threading in your applications. 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. Library routines are provided for:

The multi-threading library routines are described in the chapter Library Routines for Multi-threading.

3.3.1 Thread-control Routines

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.

You should terminate any thread created using these library routines by one of the following methods:

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:

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:
 call "CBL_THREAD_SELF" using thread-id
     on exception
     *> no cbl_thread routines support
 end-call
 if  return-code = 1008
     *> running in a single-threaded-only rts
 end-if

3.3.2 Thread-synchronization Routines

Server Express 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. The multi-threading library routines are described in the chapter Library Routines for Multi-threading.

3.3.3 Routines for Handling Thread-specific Data

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 multi-threading library routines are described in the chapter Library Routines for Multi-threading.

3.3.4 Initializing Applications

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; for example:

 Working-Storage Section.
 01 thread-id     usage pointer.

*> Initialization code executed while in
*> single-threaded mode to check if the 
*> run-time system supports multi-threading 
*> or the CBL_THREAD_ routines.
     call 'CBL_THREAD_SELF' using thread-id
      on exception
*> No cbl_thread routines support
     end-call
     if return-code = 1008
        *> Running in a single-threaded rts
     end-if

The code above uses run-time library routine CBL_THREAD_SELF to determine if the run-time system supports multi-threading.

After the application has determined that the run-time system supports multi-threading, all synchronization primitives should 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, MERANT 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:

 Working-Storage Section.
 01 first-flag         pic x comp-x value 1.
    88 first-time      value 1.
    88 not-first-time  value 0.
  
*> Initialization code executed while in 
*> multi-threaded mode. Ensures that program 
*> local data is initialized properly.

     if first-time then
         call 'CBL_THREAD_PROG_LOCK'
         if first-time then
*> Initialize program local data and synchronization
*> objects
             ...
             set not-first-time   to true
         end-if
         call 'CBL_THREAD_PROG_UNLOCK'
     end-if

The first-time level-88 data item is double-checked. This is a good optimization technique in multi-threaded applications. The idea of the optimization is to avoid the overhead of locking a mutex if the intended action has already been performed. In this case, if multiple threads enter the program before it is properly initialized, they will find that first-time is true and so issue a call to CBL_THREAD_PROG_LOCK. However, only one will acquire the lock while first-time is still true. This is the thread that will do the initialization.

After the initialization is completed, and while the initializing thread still owns the lock, the first-time flag is set to false. All successive threads acquiring the lock can see that the initialization has already been done, as first-time is now false, and unlock the program immediately. Any following threads that enter after initialization can also see that the first-time flag is false and will not attempt to acquire the program lock (and so reduce program entry overhead).

The flag that specifies whether initialization has occurred should as simple as possible (that is, a single-byte data item).

3.4 Manipulating Threads

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, enabling any multi-threaded application to take full advantage of the advanced facilities provided by the run-time system.

The multi-threading library routines are described in the chapter Library Routines for Multi-threading.

3.4.1 Thread Handles

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 an other-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.

3.4.2 Creating and Terminating a Thread

Threads are created by one of the following methods:

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 deal only with those here.

The starting point for a thread must be a non-nested Program-Id, a COBOL entry-point or, 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 only provides a return value and terminates the thread. It is equivalent to:

call 'CBL_THREAD_EXIT' using by value address of thread-parm.

A STOP RUN statement in other-language threads or in the main thread of a run-unit not created by CBL_THREAD_CREATE waits for all active COBOL threads to finish and then terminates the run-unit.

Return values from threads are always pointers. This enables 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:
$set reentrant 

 Data Division.
 Working-Storage Section.
 01 thread-handle         usage pointer.
 01 thread-return         usage pointer.
 Linkage Section.
 01 thread-parm           picture x(32).
 01 thread-return-record  picture x(32).
 Procedure Division.
*> Starting point
     call 'CBL_THREAD_CREATE' using    
                  'CREATED'
                  'This is a 32 character parameter'
                  by value 0  *> Optional parameter size
                           1  *> Flag to create non-detached
                           0  *> Default priorities
                           0  *> Default stack
                  by reference thread-handle
     if return-code = 0 
         call 'CBL_THREAD_WAIT' using 
                                  by value thread-handle
                                  by reference thread-return
         set address of thread-return-record 
                                            to thread-return
         display thread-return-record
     end-if
     stop run.
 entry "CREATED" using thread-parm.
     display thread-parm
     stop run returning address of thread-parm.

This application 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 only provides a return value and terminates the thread. It is equivalent to:

call 'CBL_THREAD_EXIT' using by value address of thread-parm.

3.4.3 Canceling a Thread

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 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:
****************************
* 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 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
*>
*> Starting point
     call 'CBL_THREAD_CREATE' using    
                     'SUBPROG '  
                     by value 0   *> No parameters
                              0   *> Optional parameter size
                              0   *> Flag to create detached
                              0   *> Default priority
                              0   *> Default stack
                     by reference sub-handle
     if  return-code not = 0 
         display 'unable to create thread'
         stop run
     end-if
*>
*> 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'
     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.

Despite being rather long, this example program only establishes handshaking for thread and application termination. Before discussing this example, 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.

Consider the following points about the example application:

3.4.4 Suspending a Thread

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 enable another process or thread to have the CPU before polling the input queue again. You can do this 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 enables 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.

3.4.5 Identifying a Thread

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.

Consider the following points about this example:

3.4.6 Threads in Other Languages

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:


Note: All the multi-threading functions supported by the run-time system detect handles created by threads in other languages, and report 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).


3.5 Canceling Called Programs

As the CANCEL operation takes a finite time to complete it is possible for a thread to enter the canceled module before the CANCEL operation has finished. You are advised, therefore, not to use the CANCEL statement in multi-threaded applications.

3.6 Optimizations and Programming Tips

This section describes programming tips and program optimizations.

The Compiler directives available for multi-threaded applications can affect the performance of your multi-threaded and single-threaded applications.

To optimize the speed of your program:

You should be aware of the following when creating multi-threaded programs:


Copyright © 2000 MERANT International Limited. All rights reserved.
This document and the proprietary marks and names used herein are protected by international law.

PreviousSynchronizing Execution and Resolving Contention Multi-threading Compiler DirectivesNext