Jump to content

CPGuide - Program Execution Control

From EDM2
Revision as of 18:01, 23 April 2020 by Ak120 (talk | contribs)

Reprint Courtesy of International Business Machines Corporation, © International Business Machines Corporation

Control Program Programming Guide and Reference
  1. Introduction to the Control Program
  2. Control Program Functions
  3. Keyboard Functions
  4. Mouse Functions
  5. Video Functions
  6. Data Types
  7. Errors
  8. Debugging
  9. Kernel Debugger Communications Protocol
  10. Device I/O
  11. Dynamic Linking
  12. Error Management
  13. Exception Management
  14. Extended Attributes
  15. File Management
  16. File Names
  17. File Systems
  18. Generic IOCtl Commands
  19. Memory Management
  20. Message Management
  21. National Language Support
  22. Pipes
  23. Program Execution Control
  24. Queues
  25. Semaphores
  26. Timers
  27. Notices
  28. Glossary

Multitasking is the ability of OS/2 to manage the execution of more than one application at a time. A multitasking operating system, such as OS/2 enables users to run many applications simultaneously.

For the programmer, OS/2 supports two types of multitasking. An application can start other programs, in separate processes, that will execute concurrently with the application. These programs can be a new copy of the application, a related program that is designed to work with the application, or an unrelated program. Running multiple processes is the first type of multitasking provided for programmers.

Running multiple threads is the second type of multitasking supported by OS/2. OS/2 enables applications to run multiple threads of execution within the same process; separate activities can be multitasked within the application. An example of multiple-thread multitasking would be for the application to dispatch a separate subroutine to load a file from the disk, and have the subroutine execute at the same time the main program continues to monitor and respond to user input.

This chapter describes processes, threads, and sessions, and the OS/2 functions used to create and manage them. Additionally, there is a section describing CPU scheduling.

The following topics are related to the information in this chapter:

  • Memory
  • Semaphores
  • Queues
  • Pipes
  • Exception handling
  • Debugging

About Program Execution Control-Thread, Processes, and Sessions

To successfully use multitasking-multiple processes and multiple threads-in your programs, you need to understand the difference between a thread, a process, and a session.

A thread is a dispatchable unit of execution that consists of a set of instructions, related CPU register values, and a stack. Each process has at least one thread, called the main thread or thread 1, and can have many threads running simultaneously. The application runs when OS/2 gives control to a thread in the process. The thread is the basic unit of execution scheduling.

A process is the code, data, and other resources-such as file handles, semaphores, pipes, queues, and so on-of an application in memory. OS/2 considers every application it loads to be a process. System resources are allocated on a per-process basis.

A session is one (or more) processes with their own virtual console. (A virtual console is a virtual screen-either a character-based, full screen or a Presentation Manager window-and buffers for keyboard and mouse input.)

OS/2 supports up to 255 concurrent sessions and up to 4095 processes. OS/2 supports a system-wide maximum of 4095 threads, but the number of threads available in a single process will be lower, and will vary, because of resource usage within the process.

Threads

Applications always have at least one thread of execution-thread 1. Using multiple threads of execution, an application can do several things at the same time.

For example, a simple Presentation Manager application consists of a single process with two threads:

  • A user interface thread that listens and responds to user requests, and that queues work for the second thread
  • A processing thread that handles lengthy processing

OS/2 creates the first thread of execution for a process when it starts the executable file. To create another thread of execution, a thread calls DosCreateThread, specifying the address within the program module where the thread begins asynchronous execution. Although a thread can execute any part of the application, including a part being executed by another thread, threads typically are used to execute separate sections of the application. By using several threads, the system can distribute the available CPU time and enable an application to carry out several tasks simultaneously. For example, an application can load a file and prompt the user for input at the same time.

Each thread in a process has a unique stack and register context. Threads shares the resources of the process with the other threads in the process. For example, threads in the same process have access to the memory spaces of other threads within the process. However, threads of one process do not have access to the data spaces of other processes.

Each thread has a priority, that determines the amount of CPU time the thread is allocated. Threads inherit the priority of the thread that creates them. The priority of a thread can be changed by the application; see Changing the Priority of a Thread for details.

An application can use DosSuspendThread and DosResumeThread to suspend and resume the execution of a given thread. When an application suspends a thread, the thread remains suspended until the application calls DosResumeThread.

When an application has more than one thread, it might be necessary to ensure that one thread is finished executing before another thread uses a shared resource, such as a disk file. DosWaitThread causes the application to wait until a specific thread has finished. DosWaitThread can also be used to determine the state of a thread; the function can return immediately with an error value if the identified thread is still running.

A thread ends when it calls DosExit.

Processes

An OS/2 application that has been loaded into memory and prepared for execution is called a process. As mentioned earlier, a process consists of the code, data, and other resources (for example, open file handles) that belong to the application. Each process has at least one thread, called the main thread or thread 1.

When OS/2 runs an application, it confirms that the process code and data are in memory and that the main thread's registers and stack are set before starting the application. Each application has access to all resources of the computer, such as memory, disk drives, screen, keyboard, and the CPU itself. The system carefully manages these resources so that applications can access them without conflict.

A process can have more than one thread. OS/2 creates the first thread of execution for a process when it starts the executable file. More threads can be created with DosCreateThread. Each thread runs independently, with its own stack and register values. Unless the application changes a thread's priority, each thread gets a slice of the CPU in a round-robin strategy. All the threads in a process share the application's globally defined variables and other resources (open file handles, and so on).

A process or thread ends when it calls DosExit. OS/2 automatically closes any files or other resources left open by the process when the process ends. When a thread ends, however, any open resources remain open until another thread closes them or the process ends. A process can direct OS/2 to carry out other actions when the process ends, by using DosExitList to create a list of termination functions. OS/2 calls the termination functions, in the order given, when the process is about to end. If the thread has registered any exception handlers, the exception handlers will also be called before the thread ends.

Creating Processes

An application can load and execute other applications by using DosExecPgm. The new application, once loaded, is called a child process. The process that starts the new application is called the parent process.

A child process is like any other process. It has its own code, data, and threads. The child process inherits the resources-such as file handles, pipes, and queues-that belong to the parent process at the time the child process is created, although not necessarily with the same access rights. The parent process can place restrictions on the access of the child process to these resources:

  • Files are inherited except for files that were opened with no inheritance indicated.
  • Pipes are inherited.

Assuming that the parent process gives the child process appropriate access rights, the child process can use the inherited resources without preparing them. For example, if the parent process opens a file for reading and then starts a child process, the child process can read from the file immediately; it does not have to open the file. However, once the child process is created additional resources that the parent process creates are not available to the child process. Similarly, resources the child process creates are not available to the parent process.

The parent process also has control over the meanings of standard input, output, and error for the child process. For example, the parent can write a series of records to a file, open the file as standard input, open a listing file as standard output, and then execute a sort program that takes its input from standard input and writes to standard output.

Note that memory is not included in the list of things that a child process can inherit from its parent process. The child process is created with its own process address space that is separate and distinct from the memory of the parent process. A new linear address space is built for the new process. The only way for a parent process and a child process to access the same memory is to set up a shared memory area.

The executable file of the child process can be started either synchronously or asynchronously to the parent process. If the parent process starts the child process running synchronously, the parent process is suspended and waits until the child process ends before continuing. A child process running asynchronously executes independently of the parent process (that is, both run at the same time). The parent process specifies how the child process is to run by setting a parameter in the call to DosExecPgm.

The OS/2 command processor, CMD.EXE, starts most child processes synchronously. The parent process waits for each child process to end before it prompts the user for the next command. The command processor also enables the user to start asynchronous applications by using the DETACH command. When the user detaches an application, the command processor starts the application asynchronously, in the background, and continues to prompt for input.

Process Termination

A parent process can use DosWaitChild to determine the termination status of a child process that is running independently. The parent process can have one of its threads call DosWaitChild to wait for completion of the child process while other threads of the parent continue processing.

If the child has started another process, DosWaitChild waits for the completion of any grandchild processes before returning, but does not report their status. If the specified child process has multiple threads, DosWaitChild returns the result code of the last DosExit request.

If there are no child processes, either active or ended with a return code, DosWaitChild returns with an error code. If no child processes have ended, DosWaitChild can optionally wait until one ends before returning to the parent.

Process Exit Lists

Because any process can end any other process for which it has a process identifier, applications might lose information if a process ends the application before it can save its work. To prevent this loss of data, you can create a list of functions to clean up data and files before OS/2 ends the process. This list is called an exit list. OS/2 maintains an exit list for each process and calls these functions whenever the application is ended, whether by another process or by itself.

You call DosExitList to add to the exit list a routine that is to be given control when a process is ended (or finishes its execution). Multiple routines can be added to the list. When the process is ending, OS/2 transfers control to each address on the list.

Exit-list functions perform clean-up operations on resources. For example, an exit-list function can be used in a dynamic link library module to free resources or clear flags and semaphores when a client program has ended.

Multitasking with Threads and Multitasking with Processes

The creation and termination of a process is relatively slow compared to the creation and termination of a thread, and is more costly in terms of system resources.

For example, sharing data and resources between processes requires shared memory and the mechanisms of interprocess communication; threads, on the other hand, have full access to the memory and other resources that belong to the process the threads are part of and can be coordinated using semaphores. For these reasons, thread-to-thread task context switches are faster than process-to-process context switches.

Because OS/2 can create and execute threads more quickly than processes, the preferred multitasking method for applications is to distribute tasks among threads in the same process instead of among processes.

Sessions

OS/2 uses sessions to help the user move from one application to the next without disrupting the screen display of an application.

A session consists of at least one process and a virtual console-buffers for keyboard and mouse input and either a character-based, full screen or a Presentation Manager window. When the system creates a session, the process in the session displays output in the screen or window. The user can view the output and supply input by moving to the session. The user moves to a session by pressing the Alt+Esc key combination, by selecting the title of the session from the Window List, or, for windowed sessions, by clicking the mouse in the session window.

A child session is under the control of the session that creates it. The session that starts the child session is called the parent session. Any process in the parent session can exercise control over a child session.

An unrelated session is not under the control of the session that started it. The process that creates the unrelated session cannot select it, make it nonselectable, bind it, or end it, nor can any other session. DosStartSession does not even return a session identifier when an unrelated session is started. Unrelated sessions are controlled entirely by the user. When OS/2 starts new sessions, it starts them as unrelated sessions.

Creating Sessions

A process creates a new session by using DosStartSession. DosStartSession enables an application to start another session and to specify the name of the application to be started in that session.

DosStartSession also specifies which of the five session types is to be started:

  • Full screen, protect mode
  • Text windowed, protect mode
  • Presentation Manager (PM)
  • Full screen DOS Session
  • Windowed DOS Session

Protect mode applications run in full screen and text windowed sessions, PM and AVIO applications run in PM windows, and DOS applications run in full screen DOS Sessions and windowed DOS Sessions.

OS/2 applications running in any of the OS/2 session types-full screen, text windowed, and PM-can start sessions of any other type, including DOS Sessions. DOS Session applications cannot start other sessions.

An application can start another process in a separate session when the application will not manage any I/O done by the process. For example, an application that starts an unrelated application could start it in a separate session.

A session can be started as a related or an unrelated session. A related session is called a child session, and the session starting the child session is called the parent session. An application can control its child sessions by using the session identifier returned by DosStartSession with the DosSetSession, DosSelectSession, and DosStopSession. If an application starts an unrelated session, the new session cannot be controlled by the application. The Related field in the STARTDATA structure specifies whether the new session is related to the session calling DosStartSession.

After a process has started a child session, no other process in its session can start a child session until all dependent sessions started by this process have ended.

When a session is created, the title specified in the function call (or the application name if no title is specified) is added to the Window List.

DosStartSession can be used to start either a foreground or a background session, but a new session can be started in the foreground only when the caller's session, or one of the caller's descendant sessions, is currently executing in the foreground. The foreground session for windowed applications is the session of the application that owns the window focus.

Termination Queues

The parent session must create a termination queue prior to specifying the queue name in a call to DosStartSession. OS/2 will continue to notify the parent session through the specified queue as long as the session calling DosStartSession remains a parent session. In other words, when all the child sessions for a particular parent session end, the termination queue is closed by OS/2. An existing queue name must be specified on the next DosStartSession call if the caller wants to continue receiving termination notification messages.

OS/2 writes a data element to the specified queue when any child session ends. The queue is posted regardless or who ends the child session (the child, the parent, or the user) and whether the termination is normal or abnormal.

A parent session calls DosReadQueue to receive notification when a child session has ended. The word that contains the request parameter, returned by DosReadQueue, will be 0. The data element has the following format:

Termination Queue Element Format
Size Description
WORD Session ID of the child session that ended
WORD Result code

The process that originally called the DosStartSession request should call DosReadQueue, with the NOWAIT parameter set to 0. This is the only process that has addressability to the notification data element. After reading and processing the data element, the caller must free the segment containing the data element by calling DosFreeMem.

When a child session ends, the result code returned in the termination queue's data element is the result code of the program specified in the DosStartSession call, providing either one of the following is true:

  • The program was run directly, with no intermediate secondary command processor
  • The program is run indirectly through a secondary command processor, and the /C parameter is specified

When a child session is running in the foreground at the time it ends, the parent session becomes the new foreground session. When a parent session ends, any child sessions are ended. When an unrelated session ends in the foreground, OS/2 selects the next foreground session.

Child Session Control

A session can be either a child session or an unrelated session. A child session is under the control of the processes in the session that creates it (the parent session). A process can select, set, or stop a child session by using DosSelectSession, DosSetSession, or DosStopSession, respectively. DosStartSession returns a unique session identifier for the child session for use in these functions.

A session can run in either the foreground or background. A process can create a foreground session only if the creating process or one of its descendant sessions is executing in the current foreground session. A process can move a child session to the foreground by selecting the child session using the session identifier and calling DosSelectSession. A process can make a child session nonselectable by using DosSetSession to change the SelectInd field in the STATUSDATA structure. This prevents the user from selecting the session from the Window List but does not prevent a process from selecting the child session by using DosSelectSession.

A process can bind a child session to its own session by using DosSetSession. Binding a session causes that session to move to the foreground whenever the user selects the parent session from the Window List.

A parent session can use a session identifier with the DosSetSession function only if the parent session created the child session associated with that identifier. It cannot use identifiers for child sessions created by other parent processes. This is true for all session management functions.

Although a child session is related to the session that started it, the processes in the child and original sessions are not related. This means that even though DosStartSession supplies the process identifier of the process in the child session, the process identifier cannot be used with OS/2 functions such as DosSetPriority.

Child Session Termination

A parent session can stop a child session by using DosStopSession. Stopping the child session ends the processes in that session. It also stops any sessions related to the child session. If a child session is in the foreground when it is stopped, the parent session becomes the foreground session. DosStopSession breaks any bond that exists between the parent session and the specified child session.

A process running in the session specified in the call to DosStopSession can ignore the request to end. If this happens, DosStopSession still returns 0 (indicating success). The only way to be certain that the child session has ended is to wait for notification through the termination queue specified in the call to DosStartSession. OS/2 writes a data element into the specified queue when the child session ends. The process in the parent session must call DosReadQueue to retrieve this data element, which contains the session identifier for the child session and the return value for the process in the child session. Only the process that created the child session can read the data element.

About CPU Scheduling

OS/2 performs prioritized, preemptive, multitasking. Prioritized means that OS/2 does not divide CPU time equally among all threads. All programs do not get equal access to the CPU. A prioritizing, time-slicing strategy is used to allocate access to the CPU among competing threads. Each thread has a priority and OS/2 runs the highest priority thread that is ready to run. Programs with higher priorities (a real-time robotic application, for example), are given access to the CPU before programs with lower priorities. If a thread with a higher priority than the currently running thread becomes ready to run, the current thread is stopped immediately, or preempted, and the higher priority thread is given the CPU. The lower priority thread does not get to complete its time slice. Threads of equal priority are given CPU time in a round-robin manner.

Preemptive means that the multitasking activity needs no cooperation from the executing programs. OS/2 maintains control over executing programs, and stops, or preempts, them when their time slice with the CPU is over or when a higher priority program is ready to run.

CPU scheduling is based on four priority classes-Time Critical, Fixed-High, Regular, and Idle-Time. Each class has 32 levels of execution ordering. Scheduling parameters are user-selectable at the time the system is started or can be varied dynamically based on system load.

Depending on a thread's priority class and level, OS/2 periodically gives each thread in each process a small slice of CPU time. Threads with higher priorities always run before threads having lower priorities. A thread runs until its time is up or until a thread with a higher priority is ready to run. At that time, OS/2 preempts the thread and starts another thread. Threads can also voluntarily relinquish the CPU (for example, by calling DosSleep).

The amount of time in each time slice is defined by the TIMESLICE command in the CONFIG.SYS file. The TIMESLICE command can be used by the user to customize the size of the time slices that a thread gets. The default is for OS/2 to dynamically vary the size of the time slice based on the activity of the thread and the overall system load.

When a thread is created (using DosCreateThread), it inherits the priority of the thread that started it. DosSetPriority enables threads to change their priority classes and levels in response to changes in their execution environments. DosSetPriority enables a thread to change its own priority, or the priority of any thread within its process. DosSetPriority also enables changing priorities for the entire process and for descendant processes. Within each class, the priority level of a thread can vary because of a DosSetPriorty request or, if dynamic priority variation is being used, because of action taken by OS/2.

Priority Classes

OS/2 uses four priority classes to determine when a thread receives a time slice:

Priority Classes
Priority Description
Time-critical Highest priority. For use when response time is critical.
Fixed-high Used by threads that provide service to other threads. This priority class is to be used when it is desirable that the thread not be too sensitive to the foreground/background boost provided by dynamic priority variation. It is meant for programs that need to execute before regular programs, but without the immediate response time requirement called for by time-critical threads.
Regular Default priority. Most threads belong in this class.
Idle-time Lowest priority. This priority only gets CPU time when there is no other work to do.

A time-critical thread always receives a time slice before a fixed-high thread, a fixed-high thread always receives a time slice before a regular thread, and a regular thread always receives a time slice before an idle-time thread.

Time-Critical Threads

Time-critical threads have the highest priority class and execute before any fixed-high, regular, or idle-time threads.

The time-critical class is for threads that must react to events outside the system. For example, in a communications application, a thread responsible for reading data from the communications device needs enough time to read all incoming data. Because more than a regular time slice might be needed, a time-critical classification ensures that the thread gets all the time required.

Time-critical threads have a static priority that is not varied by OS/2. They are scheduled among themselves in priority level order, with round-robin scheduling of threads of equal priority.

Time-critical threads must be executed quickly, then free the CPU for other work until another time-critical event occurs. This is important to maintain good interactive responsiveness to the user and enable communications and other time critical applications to run concurrently. The time-critical activity should, when possible, be in a thread separate from the rest of the application, to isolate and minimize the time spent processing at the time-critical level. A good rule of thumb is that a time-critical thread should consist of no more than about 20,000 assembly language instructions.

Fixed-High Threads

A fixed-high thread has a priority class that is lower than time-critical but executes before any regular or idle-time threads. This class of threads should be used to provide service for other threads where it is desirable that the thread not be too sensitive to the foreground/background boost provided by dynamic priority variation. A messaging thread, would be a good example of this type of thread.

OS/2 varies the priority of a fixed-high thread around a base value according to the activity of the thread and the system at any point in time. The base value can be set by the thread itself.

Regular Threads

A regular thread is the class that the majority of threads fall into. No explicit action is necessary by the application to run at this priority, it is the default.

OS/2 varies the priority level of a regular thread around a base value according to the activity of the thread and the system at any point in time. The base value can be set by the thread itself.

Idle-Time Threads

An idle-time thread is one with very low priority that executes only when there are no regular, fixed-high, or time-critical threads to execute. Idle-time threads get CPU time only when there is no other work to do. The idle-time class is for threads that need very little CPU time.

Idle-time threads have a static priority that is not varied by OS/2.

Priority Levels

Within each class, OS/2 maintains a priority level for a thread. For each of the four priority classes, there are 32 priority levels-0 to 31. A thread with priority level 31 always receives a time slice before a thread with priority level 30, and so on.

If two or more threads have the same priority level, OS/2 distributes the CPU time equally by using a round-robin scheme; that is, OS/2 gives a time slice to first one, then another, and so on, and then goes back to the first. A thread can use DosSetPriority to change its own priority or the priority of any other thread within its process.

Although an application can set the priority level of a thread at any time, only applications that use more than one thread or process should do so. The best use of priority is to speed up threads or processes on which other threads or processes depend. For example, an application might temporarily raise the priority of a thread loading a file if another thread is waiting for that file to be loaded. Because the priority of a thread is relative to all other threads in the system, raising the priority of the threads in an application merely to get the extra CPU time adversely affects the overall operation of the system.

There are other ways to affect the amount of CPU time a thread is given. A thread can define a critical section of code by using DosEnterCritSec and DosExitCritSec. While inside the critical section of code, a thread cannot be preempted by any other thread within its process (threads in other processes are still given their time slices). Using critical sections enables threads to get more CPU time, while not unduly affecting the rest of the system.

The priority class and priority level are set using DosSetPriority. The priority class is changed by passing the new priority class to the function. The priority level, however, is changed by passing a value, called the priority-delta, that is added to the existing priority level to produce the new priority level; changes to the priority level are relative to the current priority level. Specifying a positive priority-delta increases the priority level, enabling the thread to obtain more CPU time than it normally would. A negative priority-delta decreases the priority level, giving the thread less CPU time than it would normally receive. The value is restricted to the valid range, based upon the current priority class of the process.

If you change the priority level without changing the priority class, the priority-delta is relative to the current priority level. However, if you change the priority class at the same time that you change the priority level, the priority-delta value is relative to 0. Whenever DosSetPriority is called with a class specification, but no value is specified for priority-delta, the base priority level defaults to 0.

The process identifier parameter of DosSetPriority specifies which process is affected by the call. A process can change the priority of itself, of any process that is a descendant of itself, or of one of its threads.

A thread can change the priority of any thread within its current process. When a thread changes the priority of threads in a descendant process, however, only those threads running at the default priority will be changed. You cannot change the priority of a thread in a child process that has already changed its priority from the default.

The initial thread of execution for an application is given a regular class priority that varies by the system. When a thread is created, it is initially dispatched in the same class and priority as the thread that started it. A child process inherits the priority of the thread in the parent process that creates it.

Priority Guidelines

Within the two most common priority classes-time-critical and regular-there are certain broad guidelines recommended for choosing the priority level for a program.

TIME-CRITICAL CLASS
The guidelines for level within the time critical class are set to maximize the number of different applications that can successfully multitask in an OS/2 system. The guidelines are described in the following table.
Recommended Priority Levels for Time-Critical Threads
Activity Range of Recommended Priority Levels Justification/Comments
Robotics/Real time process control 20-31 OS/2 systems can be used on manufacturing lines to control equipment or in other real time process control applications. In this case a slow response from the PC could cause expensive damage to equipment or even human injury. Therefore the highest priority levels should be reserved for these applications.
Communications 10-19 In communications, the inability to get the processor could cause a loss of data or communications sessions. Therefore this class of applications is next highest.
Other 0-09 Other threads might need to preempt the foreground in special cases (for example, Control-Break). These should be set below the other 2 classes.

In general, application performance is not a good reason to make a thread time critical.

REGULAR CLASS
In cases where explicit priority levels are set, they should follow the guidelines listed below.
Recommended Priority Levels for Regular Priority Threads
Activity Range of Recommended Priority Level Justification
Communications 26-31 Communications should take priority over other background processing to increase overlap with transmission and processing on the partner PC or host. This gives the best system performance.
Other 0-25. If an application has multiple threads it might be necessary to set them to several different priorities to optimize the order in which they run. A range of priority levels is provided to facilitate this. (The default priority is regular class, level = 0.)

Dynamic Priority Alteration

OS/2 can be configured to dynamically alter the priority of a process. The PRIORITY statement in CONFIG.SYS can be set to either ABSOLUTE or DYNAMIC. If PRIORITY specifies the ABSOLUTE option, all processes receive CPU time based on the priority established by calls to DosSetPriority. If the PRIORITY command in the CONFIG.SYS file specifies the DYNAMIC option, OS/2 adjusts process priorities based on system load and process activity, and on whether the process is in the foreground. DYNAMIC is the default setting; if the PRIORITY command is not specified, the system uses DYNAMIC priority. DYNAMIC is designed to gives the best overall system performance under most conditions.

When dynamic priority variation is enabled, OS/2 grants higher priority to the foreground process than to background processes. System load and process activity are also taken into consideration. The priority of the process consists of a computed priority value that is based upon the display status of the process (foreground or background), and its recent I/O and CPU time usage history. When dynamic priority variation is enabled, I/O priority boosts are generated for keyboard input, window, foreground, processor starvation, device I/O, and DOS Session interrupts. This ensures that the foreground process-the process most likely to be in use-receives enough CPU time to provide quick response to user input.

There are times when dynamic priority variation can interfere with a multi-threaded application's execution. For example, if you are doing a lot of keyboard input on a thread, its priority will get boosted and other threads may not get enough CPU time. A communication thread is an example of a time sensitive background thread which would be one case. The solution is to either set PRIORITY = ABSOLUTE or to call DosSetPriority on a regular basis to keep the threads priority at the desired level.

Altering the Size of the Time Slice

The TIMESLICE command in CONFIG.SYS sets the minimum and maximum amount of processor time allocated to processes and programs for both OS/2 and DOS sessions.

The first parameter selects the minimum TIMESLICE value in milliseconds. This value is the minimum amount of time a thread is to be processed before yielding the processor to a thread of the same priority level. This value must be an integer greater than or equal to 32.

The second parameter selects the maximum TIMESLICE value in milliseconds. The value of this parameter is the maximum amount of time a thread can be processed before yielding processor time. This value must be an integer greater than or equal to the minimum value, and less than 65536.

The default is dynamic time slicing, which varies the size of the time slice depending on system load and paging activity. Dynamic time slicing is designed to give the best performance in most situations.

Using Processes

An OS/2 application that has been loaded into memory and prepared for execution is called a process. A process is the code, data, and other resources of the application, such as the open file handles, semaphores, pipes, queues and so on. OS/2 considers every application it loads to be a process.

Each process has at least one thread, called the main thread or thread 1. The application runs when the system scheduler gives control to a thread in the process. For more on thread management, see Using Threads.

Note
In the example code fragments that follow, error checking was left out to conserve space. Applications should always check the return code that the functions return. Control Program functions return an APIRET value. A return code of 0 indicates success. If a non-zero value is returned, an error occurred.

Starting a Child Process

You start a process by calling DosExecPgm. The process you start is a child of the calling, or parent, process and inherits many of the resources owned by the parent process, such as file handles.

DosExecPgm creates a process environment from an executable file. The target program is loaded into storage, and it begins execution.

The following code fragment starts an application named ABC:


   #define INCL_DOSPROCESS       /* Process and thread values */
   #include <os2.h>

   CHAR szFailName[CCHMAXPATH];
   RESULTCODES resc;
   DosExecPgm(szFailName,           /* Object-name buffer  */
              sizeof(szFailName),   /* Length of buffer    */
              EXEC_SYNC,            /* Sync flag           */
              (PSZ) NULL,           /* Argument string     */
              (PSZ) NULL,           /* Environment string  */
              &resc,                /* Address for result  */
              "ABC.EXE");           /* Name of application */


In this example, ABC runs synchronously (as specified by EXEC_SYNC). This means the parent process temporarily stops while the child process runs. The parent process does not continue until the child process ends.

Starting an Asynchronous Child Process

To start a child process and enable it to run asynchronously (that is, without suspending the parent process until the child process ends), you use the EXEC_ASYNC constant in a call to DosExecPgm. If you start a process in this way, the function copies the process identifier of the child process to the codeTerminate field of the RESULTCODES structure that is returned by DosExecPgm. You can use this process identifier to check the progress of the child process or to end the process.

You can also run a child process asynchronously by using DosExecPgm with the EXEC_ASYNCRESULT constant. In addition to causing DosExecPgm to return to the parent process immediately, this constant also directs OS/2 to save a copy of the termination status when the child process ends. This status specifies the reason the child process stopped. The parent process can retrieve the termination status by using DosWaitChild.

The following code fragment starts the program SIMPLE.EXE, and then waits for it to finish. It then prints the termination code and the return code.

    #define INCL_DOSPROCESS       /* Process and thread values */
    #include <os2.h>
    #include <stdio.h>

    #define START_PROGRAM "SIMPLE.EXE"

    CHAR         szLoadError[100];
    PSZ          pszArgs;
    PSZ          pszEnvs;
    RESULTCODES  rcReturnCodes;
    APIRET       ulrc;

    ulrc = DosExecPgm(szLoadError,         /* Object name buffer           */
                      sizeof(szLoadError), /* Length of object name buffer */
                      EXEC_SYNC,           /* Asynchronous/Trace flags     */
                      pszArgs,             /* Argument string              */
                      pszEnvs,             /* Environment string           */
                      &rcReturnCodes,      /* Termination codes            */
                      START_PROGRAM);      /* Program file name            */

    printf("Termination Code %d  Return Code %d \n",
           rcReturnCodes.codeTerminate,
           rcReturnCodes.codeResult);

/*----------------SIMPLE.EXE------------------*/

    #define INCL_DOSPROCESS       /* Process and thread values */
    #include <os2.h>
    #include <stdio.h>

    #define RETURN_CODE 0

    main()
    {
        printf("Hello!\n");
        DosExit(EXIT_PROCESS,     /* End the thread or process */
                RETURN_CODE);     /* Result code               */
    }

Starting a Background Process

You can start a child process in the background by specifying the EXEC_BACKGROUND constant in DosExecPgm. A background process runs independently of the parent process and is called a detached process. A detached process should not require any input from the keyboard or output to the video screen, but it can use interprocess communication, such as pipes, queues, and shared memory.

The following code fragment starts the program BATCH.EXE in the background.

    #define INCL_DOSPROCESS       /* Process and thread values */
    #include <os2.h>
    #include <stdio.h>

    #define START_PROGRAM "BATCH.EXE"

    CHAR         szLoadError[100];
    PSZ          pszArgs;
    PSZ          pszEnvs;
    RESULTCODES  rcReturnCodes;
    APIRET       ulrc;

    ulrc = DosExecPgm(szLoadError,         /* Object name buffer           */
                      sizeof(szLoadError), /* Length of object name buffer */
                      EXEC_BACKGROUND,     /* Asynchronous/Trace flags     */
                      pszArgs,             /* Argument string              */
                      pszEnvs,             /* Environment string           */
                      &rcReturnCodes,      /* Termination codes            */
                      START_PROGRAM);      /* Program file name            */

    if (ulrc != 0) {
        printf("DosExecPgm error: return code = %ld",
               ulrc);
        return;
    }

Setting the Command Line and Environment for a Child Process

When you start a process, it inherits many of the resources of the parent. This includes file handles, such as the standard input and standard output files. A child process also inherits the resources of the screen group, such as the mouse and video modes, and the environment variables of the parent process.

The call to DosExecPgm determines the command line and environment that the child process receives. The fourth and fifth parameters of the function are pointers to the command line and the environment, respectively. If these pointers are NULL, the child process receives nothing for a command line and only an exact duplicate of the environment of the parent process. The parent process can modify this information by creating a string (ending with two NULL characters) and passing the address of the string to the function. The command line string must include the name of the application, followed by a NULL character, and the command line arguments, followed by two NULL characters. Any number of arguments can be passed to the child process, as long as the argument string ends with two NULL characters.

The following code fragment passes to the child process the string "test -option1 -option2" as its command line:

 #define INCL_DOSPROCESS    /* Process and thread values */
 #include <os2.h>
 
 RESULTCODES resc;
 CHAR szFailName[CCHMAXPATH];
 
 CHAR szCommandLine[] = "test\0-option1 -option2\0";
 
 DosExecPgm(szFailName,          /* Object-name buffer  */
            sizeof(szFailName),  /* Length of buffer    */
            EXEC_SYNC,           /* Sync flag           */
            szCommandLine,       /* Argument string     */
            (PSZ) NULL,          /* Environment string  */
            &resc,               /* Address of result   */
            "test.exe");         /* Name of application */

Changing the Priority of a Process

Changing the priority of a process is simply a matter of changing the priority of every thread executing in the process. For the details see the section on changing thread priorities, Changing the Priority of a Thread.

Obtaining Information about Child Processes

OS/2 creates and maintains a process information block for every process. An application can use DosGetInfoBlocks to access the process information block. This function returns a pointer to a PIB data structure, which contains the information from the process information block.

The following code fragment returns the address of the process information block of the current process. The calling thread can subsequently browse either the PIB block.

 #define INCL_DOSPROCESS     /* Process and thread values */
 #include <os2.h>
 
 PTIB     ptib;      /* Address of pointer to thread information block  */
 PPIB     ppib;      /* Address of pointer to process information block */
 APIRET   rc;        /* Return code                                     */
 
 rc = DosGetInfoBlocks(&ptib, &ppib);

DosGetInfoBlocks also returns the address of the thread information block of the current thread.

Waiting for a Child Process to End

You can synchronize the execution of a process with the execution of one of its child processes by calling DosWaitChild. DosWaitChild does not return until the specified child process ends. This can be useful, for example, if the parent process needs to ensure that the child process has completed its task before the parent process continues with its own task.

In the following code fragment, the parent process starts a child process and then waits for the child process to finish:

 #define INCL_DOSPROCESS     /* Process and thread values */
 #include <os2.h>
 
 RESULTCODES resc;
 PID pidEnded;
 CHAR szFailName[CCHMAXPATH];
 
 CHAR szCommandLine[] = "APP\0test\0";
 
 DosExecPgm(szFailName,              /* Failed-name buffer     */
            sizeof(szFailName),      /* Length of buffer       */
            EXEC_ASYNC,              /* Sync flag              */
            szCommandLine,           /* Argument string        */
            (PSZ) NULL,              /* Environment string     */
            &resc,                   /* Address of result      */
            "APP.EXE");              /* Name of application    */
 
 DosWaitChild(DCWA_PROCESS,          /* Only the process       */
              DCWW_WAIT,             /* Waits until it is done */
              &resc,                 /* Puts the result here   */
              &pidEnded,             /* PID of ended process   */
              resc.codeTerminate);   /* Child to wait for      */

You can cause a process to wait for all its child processes to end by using the DCWA_PROCESSTREE constant in the call to DosWaitChild.

Ending the Current Process

You end the current process by calling DosExit. When you exit, the system stops the process and frees any existing resources the process owns.

In the following code fragment, DosExit is used to end the process if the given file does not exist:

    #define INCL_DOSPROCESS     /* Process and thread values */
    #include <os2.h>

    #define HF_STDERR 2         /* Standard error handle     */

    HFILE   hf;
    ULONG   ulAction, ulWritten;
    APIRET  rc;

    rc = DosOpen("SAMPLE.TXT",
                 &hf,
                 &ulAction,
                 0,
                 FILE_NORMAL,
                 FILE_OPEN,
                 OPEN_ACCESS_WRITEONLY |
                 OPEN_SHARE_DENYWRITE,
                 (PEAOP2) NULL);

    if (rc) {
        DosWrite(HF_STDERR,
                 "Cannot open file\r\n",
                 18,
                 &ulWritten);

        DosExit(EXIT_PROCESS,
                rc);
    }

EXIT_PROCESS directs DosExit to end all the threads in a process including the calling thread, thus ending the process. DosExit includes an error value that is returned to the parent process through the RESULTCODES structure specified in the DosExecPgm call that started the process. If you started the application from the command line, the command processor, CMD.EXE, makes this value available through the ERRORLEVEL variable. If another process started the application, that process can call DosWaitChild to determine the error value.

If you want to exit only from a given thread, you can call DosExit with the EXIT_THREAD constant. This will end only the calling thread; other threads in the process are not affected. If the thread you end is the last thread in the process, the process also ends. If the thread consists of a function, the thread ends when the function returns.

Terminating a Process

A process can end the execution of a descendant process by calling DosKillProcess. This causes OS/2 to send a XCPT_SIGNAL_KILLPROC exception to the target process. The child processes of the target process can also be ended.

The following code fragment ends the specified process and all child processes belonging to that process:

#define INCL_DOSPROCESS       /* Process and thread values */
#include <os2.h>

PID pidProcess;

DosKillProcess(DKP_PROCESSTREE,
               pidProcess);

In this example, the pidProcess parameter specifies which descendant process to end. The process identifier is returned by DosExecPgm in the codeTerminate field of the RESULTCODES structure when you start the child process.

The parameter DKP_PROCESSTREE in the example indicates that the specified process, pidProcess, and all of its descendant processes are to be ended.

If you specify DKP_PROCESS in a call to DosKillProcess, only the specified process is ended. Its child processes, if any, continue to run.

The process to be ended must either be the current process, or it must have been directly created by the current process with DosExecPgm for asynchronous execution. That is, a process can end itself and its descendants.

The process to be ended need not still be executing. If it has started its own child processes, but has stopped executing, its children can still be flagged for termination.

Obtaining the Termination Status of a Child Process

OS/2 saves the termination status for a process if the process was started by using the EXEC_ASYNCRESULT constant in the call to DosExecPgm. You can retrieve the termination status of the most recently ended process by using the DCWW_NOWAIT constant with DosWaitChild and setting the child process identification parameter to 0. The DCWW_NOWAIT constant directs the function to return immediately, without waiting for a process to end. Instead, the function retrieves the termination status from the process that most recently ended. If you specify a child process identification with DCWW_NOWAIT, DosWaitChild returns ERROR_CHILD_NOT_COMPLETE if the child process has not ended. Once the specified process has ended, DosWaitChild returns its termination code.

The following code fragment starts a child session (the program SIMPLE.EXE), and then retrieves the termination status from the process that most recently ended.

 #define INCL_DOSPROCESS         /* Process and thread values */
 #include <os2.h>
 #include <stdio.h>
 
 #define START_PROGRAM "SIMPLE.EXE"
 
 CHAR         szLoadError[100];
 PSZ          pszArgs;
 PSZ          pszEnvs;
 RESULTCODES  rcReturnCodes;
 ULONG        ulPid;           /* Process ID (returned)             */
 ULONG        ulTarget;        /* Process ID of process to wait for */
 APIRET       ulrc;            /* Return code                       */
 
 strcpy(pszArgs,
        "-a2 -l");   /* Pass arguments "-a2" and "-l"                         */
 
 ulTarget = 0;       /* Process ID for the most recently ended process   */
 
 ulrc = DosExecPgm(szLoadError,            /* Object name buffer                */
                   sizeof(szLoadError),    /* Length of object name buffer      */
                   EXEC_ASYNCRESULT,       /* Asynchronous/Trace flags          */
                   pszArgs,                /* Argument string                   */
                   pszEnvs,                /* Environment string                */
                   &rcReturnCodes,         /* Termination codes                 */
                   START_PROGRAM);         /* Program file name                 */
 
 if (ulrc != 0) {
     printf("DosExecPgm error: return code = %ld",
            ulrc);
     return;
 }
 
 ulrc = DosWaitChild(DCWA_PROCESS,         /* Execution options                 */
                     DCWW_NOWAIT,          /* Wait options                      */
                     &rcReturnCodes,       /* Termination codes                 */
                     &ulPid,               /* Process ID (returned)             */
                     ulTarget);            /* Process ID of process to wait for */
 
 if (ulrc != 0) {
     printf("DosWaitChild error: return code = %ld",
            ulrc);
     return;
 }

Creating an Exit List

You call DosExitList to add to the exit list a routine that is to be given control when a process is ended (or finishes its execution). Multiple routines can be added to the list. When the process is ending, OS/2 transfers control to each address on the list.

If there are multiple addresses on the list, each function gets control in numerical order (with 0 being first and 0FFH being last), based on a value supplied by the application when it calls DosExitList. In case of duplicate entries for this parameter, the routines will be executed in LIFO (last in, first out) order.

DosExitList requires a function code that specifies an action and a pointer to the function that is to receive control upon termination.

The following code fragment adds the locally defined function SaveFiles to the exit list:

 #define INCL_DOSPROCESS         /* Process and thread values */
 #include <os2.h>
 
 #define HF_STDOUT 1             /* Standard output handle    */
 
 VOID main(VOID)
 {
      .
      .
      .
     DosExitList(EXLST_ADD,
                 (PFNEXITLIST) SaveFiles);
      .
      .
      .
     DosExit(EXIT_PROCESS,
             0);
 }
 
 VOID APIENTRY SaveFiles(ULONG ulTermCode)
 {
     ULONG ulWritten;
     switch (ulTermCode) {
         case TC_EXIT:
         case TC_KILLPROCESS:
              DosWrite(HF_STDOUT,
                       "Goodbye\r\n",
                       10,
                       &ulWritten);
              break;
 
         case TC_HARDERROR:
         case TC_TRAP:
              break;
 
     }
     DosExitList(EXLST_EXIT,
                 0);
 }

Any function that you add to the list must take one parameter. The function can carry out any task, as shown in the preceding example, but as its last action it must call DosExitList, specifying the EXLST_EXIT constant. An exit-list function must not have a return value and must not call DosExit to end.

When an exit-list routine receives control, the parameter (located at ESP+4 on the stack) contains an indicator of why the process ended. The values returned are the same as those for termination codes returned by DosWaitChild or DosExecPgm requests. These values are:

TC_EXIT        (0) Normal exit
TC_HARDERROR   (1) Hard-error halt
TC_TRAP        (2) Trap operation for a 16-bit child process
TC_KILLPROCESS (3) Unintercepted DosKillProcess
TC_EXCEPTION   (4) Exception operation for a 32-bit child process

To execute the exit-list functions, OS/2 reassigns thread 1 after ending all other threads in the process. If thread 1 has already exited (for example, if it called DosExit without ending other threads in the process), the exit-list functions cannot be executed. In general, it is poor practice to end thread 1 without ending all other threads.

Before transferring control to the termination routines, OS/2 resets the stack to its initial value. Transfer is by way of an assembly language JMP instruction. The routine must be in the address space of the ending process. The termination routine at that address takes the necessary steps and then calls DosExitList with FunctionOrder=EXLST_EXIT. Control is then transferred to the next address in the invocation order of the exit list. When all such addresses have been processed, the process completes exiting. If a routine on the list does not call DosExitList at the completion of its processing, the process waits, and OS/2 prevents termination.

During DosExitList processing, the process is in a state of partial termination. All threads of the process are ended, except for the one executing the exit-list routines. To ensure good response to a user request to end a program, there should be minimal delay in completing termination. Termination routines should be short and fail-safe.

You can use DosExitList with the EXLST_REMOVE constant to remove a function from the list.

The designer of an exit-list routine must carefully consider which functions will be used by the routine. In general, calls to most OS/2 functions are valid in a DosExitList routine, but certain functions, such as DosCreateThread and DosExecPgm, are not.

Using Threads

A thread is a dispatchable unit of execution that consists of a set of instructions, related CPU register values, and a stack. Every process has at least one thread and can have many threads running at the same time. The application runs when OS/2 gives control to a thread in the process. The thread is the basic unit of execution scheduling.

Every process has at least one thread, called the main thread or thread 1. To execute different parts of an application simultaneously, you can start several threads.

A new thread inherits all the resources currently owned by the process. This means that if you opened a file before creating the thread, the file is available to the thread. Similarly, if the new thread creates or opens a resource, such as another file, that resource is available to the other threads in the process.

Creating a Thread

You use DosCreateThread to create a new thread for a process.

DosCreateThread requires the address of the code to execute and a variable to receive the identifier of the thread. The address of the code is typically the address of a function that is defined within the application.

You can pass one ULONG parameter to the thread when you start it. To pass more information to the thread, pass the address of a data structure.

You specify how you want the thread to run when you call DosCreateThread. If bit 1 of the flag parameter in the function call is 0, the thread starts immediately. If bit 1 of the flag parameter is 1, the thread starts suspended and will not run until the application calls DosResumeThread.

Each thread maintains its own stack. You specify the size of the stack when you call DosCreateThread. The amount of space needed depends on a number of factors, including the number of function calls the thread makes and the number of parameters and local variables used by each function. If you plan to call OS/2 functions, a reasonable stack size is 8192 bytes (8KB); 4096 bytes (4KB) should be the absolute minimum. If bit 1 of the flag parameter is 0, OS/2 uses the default method for initializing the thread's stack. If bit 1 of the flag parameter is 1, memory for the thread's entire stack is pre-committed.

The following code fragment creates a thread:

 #define INCL_DOSPROCESS         /* Process and thread values */
 #include <os2.h>
 #include <stdio.h>
 
 #define HF_STDOUT 1             /* Standard output handle    */
 
 VOID _System ThreadFunc(ULONG ulBeepLen);
 
 INT main(VOID)
   {
   ULONG ulBeepLen;
   TID   tidThread;
 
   ulBeepLen = 1000;
   DosCreateThread(&tidThread,       /* Thread ID returned by DosCreateThread */
                   &ThreadFunc,      /* Address of the thread function        */
                   ulBeepLen,        /* Parameter passed to thread            */
                   0,                /* Immediate execution, default stack    */
                                     /* initialization                        */
                   4096);            /* Stack size                            */
 
   DosWaitThread(&tidThread,
                 DCWW_WAIT);
   return 0;
   } /* end main */

/***************************************************/
/* ThreadFunc                                      */
/***************************************************/
 VOID _System ThreadFunc(ULONG ulBeepLen)
   {
   ULONG ulWritten; /* needed for DosWrite */
 
   DosBeep(750, ulBeepLen);
 
   DosWrite(HF_STDOUT,
            "Message from new thread\r\n",
            25,
            &ulWritten);
 
   DosExit(EXIT_PROCESS, 0);
   } /* end ThreadFunc */

A thread continues to run until it calls DosExit, returns control to OS/2, or is ended by a DosKillThread call.

In the preceding example, the thread exits when the function implicitly returns control at the end of the function.

Obtaining Information about a Thread

OS/2 creates and maintains a thread information block for each thread. An application can use DosGetInfoBlocks to access the thread information block. This function returns a pointer to a TIB data structure.

The code fragment below returns the address of the thread information block of the current thread. The calling thread can subsequently browse the TIB.

DosGetInfoBlocks also returns the address of the process information block of the current process.

Changing the Priority of a Thread

You can use DosSetPriority to change the execution priority of threads in a process. The execution priority defines when or how often a thread receives an execution time slice. Threads with higher priorities receive time slices before those with lower priorities. When a thread that is higher in priority than the currently running thread becomes ready to run, it immediately preempts the lower priority thread (the lower priority thread does not get to complete its time slice). Threads with equal priority receive time slices in a round-robin order. If you raise the priority of a thread, the thread is executed more frequently.

You can use DosSetPriority to set the priority for one thread in a process, for all threads in a process (and thus the process itself), or for threads in a child process.

A process can change the priority of any thread within itself. When a process changes the priority of threads in a descendant process, however, only those with default priorities are changed. The priority of any thread in a descendant process that has already explicitly changed its priority from the default with DosSetPriority is not changed.

In the following code fragment, DosSetPriority lowers the priority of a process to be used as a background process:

 #define INCL_DOSPROCESS       /* Process and thread values */
 #include <os2.h>
 
 PTIB ptib;    /* thread information block          */
 PPIB ppib;    /* process information block         */
 APIRET rc;    /* return code from DosGetInfoBlocks */
 
 rc = DosGetInfoBlocks(&ptib, &ppib);
 
 DosSetPriority(PRTYS_PROCESSTREE,
                PRTYC_IDLETIME,
                0,
                ppib->pib_ulpid);

DosGetInfoBlocks retrieves the process information blocks and thread information blocks. DosSetPriority then uses the process identifier to change the priority to idle time (idle-time processes receive the least attention by OS/2).

If you specify PRTYS_PROCESS when calling DosSetPriority, only the priority of the specified process changes. The priorities of all child processes remain unchanged.

If you specify PRTYS_THREAD in the call to DosSetPriority, you must specify a thread identifier as the last parameter. The priority of the specified thread changes, but the priorities of all other threads in the process remain unchanged.

Whenever DosSetPriority is called with a class specification, but no value is specified for priority-delta, the base priority level defaults to 0.

Suspending the Current Thread

You can temporarily suspend the execution of the current thread for a set amount of time by using DosSleep. This function suspends execution of the thread for the specified number of milliseconds. DosSleep is useful when you need to delay the execution of a task. For example, you can use DosSleep to delay a response when the user presses a DIRECTION key. The delay provides the user with enough time to observe the results and release the key.

The following code fragment uses DosSleep to suspend execution of a thread for 1000 milliseconds (1 second):

 #define INCL_DOSPROCESS       /* Process and thread values */
 #include <os2.h>
 
 DosSleep(1000);

Suspending and Resuming Execution of a Thread

DosSuspendThread and DosResumeThread are used to temporarily suspend the execution of a thread when it is not needed and resume execution when the thread is needed.

These functions are best used when it is necessary for a process to temporarily suspend execution of a thread that is in the middle of a task. For example, consider a thread that opens and reads files from a disk. If other threads in the process do not require input from these files, the process can suspend execution of the thread so that OS/2 does not needlessly grant control to it.

The specified thread might not be suspended immediately if it has some system resources locked that must be freed first. However, the thread is not permitted to execute further application program instructions until a corresponding DosResumeThread is called.

A thread can only suspend another thread that is within its process.

DosResumeThread is used to enable the suspended thread to resume execution.

The following code fragment temporarily suspends the execution of another thread within the same process. A subsequent call to DosResumeThread restarts the suspended thread. Assume that the thread identifier of the target thread has been placed int ThreadID already.

 #define INCL_DOSPROCESS     /* Process and thread values */
 #include <os2.h>
 #include <stdio.h>
 
 TID     tidThreadID;     /* Thread identifier */
 APIRET  ulrc;            /* Return code       */
 
 ulrc = DosSuspendThread(tidThreadID);
 
 if (ulrc != 0) {
     printf("DosSuspendThread error: return code = %ld",
            ulrc);
     return;
 }
 
 ulrc = DosResumeThread(tidThreadID);
 
 if (ulrc != 0) {
     printf("DosResumeThread error: return code = %ld",
            ulrc);
     return;
 }

Entering Critical Sections

A thread can prevent execution of any of the other threads in its process by calling DosEnterCritSec.

This function temporarily prevents a thread from being preempted by other threads within its process. The other threads in the process will not be executed until the current thread calls DosExitCritSec. This enables the calling thread to access a time-critical resource of the process.

The following code fragment enters a section that will not be preempted, performs a simple task, and then exits quickly.

 #define INCL_DOSPROCESS     /* Process and thread values */
 #include <os2.h>
 #include <stdio.h>
 
 BOOL    flag;         /* Program control flag */
 APIRET  ulrc;         /* Return code          */
 
 ulrc = DosEnterCritSec();
 
 if (ulrc != 0) {
     printf("DosEnterCritSec error: return code = %ld",
            ulrc);
     return;
 }
 
 flag = TRUE;           /* Set the flag */
 
 ulrc = DosExitCritSec();
 
 if (ulrc != 0) {
     printf("DosExitCritSec error: return code = %ld",
            ulrc);
     return;
 }

A count is maintained of the outstanding DosEnterCritSec requests. The count is incremented when a DosEnterCritSec request is made, and decremented when a DosExitCritSec request is made. A DosExitCritSec request will not cause normal thread dispatching to be restored while the count is greater than 0.

This count is maintained in a WORD-sized variable. If overflow occurs, the count is set to its maximum value, and an error is returned. The operation is not performed when this occurs.

Threads that call DosEnterCritSec must not must not make dynamic link calls within these critical sections. The dynamic link procedure could be using semaphores to serialize a resource. If a thread entering the critical section blocks another thread that already owns the resource which the dynamic link function is about to request, a deadlock occurs.

For example, threads of an application are serializing their access to a queue by means of a semaphore. A thread enters a critical section and makes a request to read the queue while another thread already has the semaphore that controls access to the queue. The thread that has the semaphore is now effectively blocked by DosEnterCritSec, and the thread that has requested the queue waits forever to access it.

Note
Thread 1 is the initial thread of execution. It handles all signals (Ctrl+C, Ctrl+Break, and KillProcess). If a signal occurs while DosEnterCritSec is active, thread 1 can begin execution to process the signal. Thread 1 must not access the critical resource that is being protected by the use of DosEnterCritSec.

Waiting for a Thread to End

An application might need to ensure that one thread has finished executing before another thread continues with its own task. For example, one thread might have to finish reading a disk file into memory before another thread can use the information. You can use DosWaitThread to suspend a thread until another thread has ended.

DosWaitThread places the current thread into a wait state until another thread in the current process has ended. It then returns the thread identifier of the ending thread.

The following code fragment creates three threads. The thread identifier for each thread is returned by DosCreateThread in the atid array. Using &atid[0] as a parameter in the call to DosWaitThread causes OS/2 to wait until the thread with that identifier (the thread running Thread2Func) ends.

 #define INCL_DOSPROCESS       /* Process and thread values */
 #include <os2.h>
 
 #define HF_STDOUT 1           /* Standard output handle    */
 
 TID tidAtid[3];
 ULONG ulWritten;
 
 DosCreateThread(&tidAtid[0],
                 Thread2Func,
                 0,
                 0,
                 4096);
 
 DosCreateThread(&tidAtid[1],
                 Thread3Func,
                 0,
                 0,
                 4096);
 
 DosCreateThread(&tidAtid[2],
                 Thread4Func,
                 0,
                 0,
                 4096);
 
 DosWaitThread(&tidAtid[0],
               DCWW_WAIT);
 
 DosWrite(HF_STDOUT,
          "The thread has ended\r\n",
          27,
          &ulWritten);

If you set the tid parameter to 0 in the call to DosWaitThread, OS/2 waits only until the next thread (any thread in the process) ends. The identifier for the ended thread is then returned in the tid parameter.

After the threads are created as in the preceding example, the following code fragment waits until one of the threads ends, and then returns its thread identifier:

 #define INCL_DOSPROCESS       /* Process and thread values */
 #include <os2.h>
 
 TID tid;
 
 tid = 0;
 
 DosWaitThread(&tid, DCWW_WAIT);

The thread identifier of the next thread to end after the DosWaitThread call is returned in the tid parameter.

You can use DosWaitThread so that you can recover thread resources when the thread ends, or to synchronize the execution of a thread with the execution of other threads.

Ending the Current Thread

To end the execution of the current thread, call DosExit, specifying the action code as 0. It is good practice to end each thread in the application individually.

If the thread that is ending is the last thread in the process, or if the request is to end all threads in the process, then the process also ends. All threads except one are ended, and that thread executes any routines in the list specified by DosExitList. When this is complete, the resources of the process are released, and the result code that was specified in the DosExit call is passed to any thread that calls DosWaitChild for this process.

In the following code fragment, the main routine starts another program, SIMPLE.EXE, and then expects a return code of 3 to be returned. SIMPLE.EXE sets the return code with DosExit.

 #define INCL_DOSPROCESS           /* Process and thread values */
 #include <os2.h>
 #include <stdio.h>
 
 #define START_PROGRAM "SIMPLE.EXE"
 #define RETURN_OK 3

 CHAR        szLoadError[100];
 PSZ         pszArgs;
 PSZ         pszEnvs;
 RESULTCODES rcReturnCodes;
 APIRET      ulrc;
 
 ulrc = DosExecPgm(szLoadError,                /* Object name buffer           */
                   sizeof(szLoadError),        /* Length of object name buffer */
                   EXEC_SYNC,                  /* Asynchronous/Trace flags     */
                   pszArgs,                    /* Argument string              */
                   pszEnvs,                    /* Environment string           */
                   &rcReturnCodes,             /* Termination codes            */
                   START_PROGRAM);             /* Program file name            */
 
 if (ReturnCodes.codeResult == RETURN_OK)      /* Check result code            */
     printf("Things are ok...");
 else
     printf("Something is wrong...");

/*----------------SIMPLE.EXE------------------*/
 
 #define INCL_DOSPROCESS          /* Process and thread values */
 #include <os2.h>
 #include <stdio.h>
 
 #define RETURN_CODE 3
 
 main()
 {
     printf("Hello!\n");
     DosExit(EXIT_THREAD,         /* End thread/process */
             RETURN_CODE);        /* Result code        */
 }

When you specify DosExit for thread 1 (the initial thread of execution started by OS/2 for this process), all of the threads in the process are ended, and the process is ended.

Ending a Thread

DosKillThread ends a thread in the current process. DosKillThread enables a thread in a process to end any other thread in the process.

DosKillThread is used to force a thread within the current process to end without causing the entire process to be ended.

 #define INCL_DOSPROCESS       /* Process and thread values */
 #include <os2.h>
 
 TID tidThread;    /* ThreadID of the thread to be ended */
 
 DosCreateThread(&tidThread,
                 ThreadFunction,
                 0,
                 0,
                 4096);
 .
 .
 .
 DosKillThread(tidThread);

DosKillThread returns to the requestor without waiting for the target thread to complete its termination processing.

It is an invalid operation to use DosKillThread to kill the current thread.

Terminating thread 1 will cause the entire process to end similar to executing DosExit on thread 1. DosKillThread will not end a thread that is suspended. Instead the suspended thread will be ended when it resumes execution. For this reason, you should not kill the main thread of an application if there are any secondary threads that are suspended.

If the target thread is executing 16-bit code or was created by a 16-bit requester, ERROR_BUSY is returned.

Using Sessions

A session consists of at least one process and a virtual console (either full screen, or Presentation Manager window, and buffers for keyboard and mouse input). An application can manage its own child sessions by using DosStartSession, DosStopSession, DosSelectSession, and DosSetSession.

Starting a Session

DosStartSession is used to start new sessions and to specify the name of the application to be started in the new session.

There are five types of sessions that you can start: full screen, text window, Presentation Manager (PM), full screen DOS Session, and windowed DOS Session. OS/2 applications running in any of the OS/2 session types-full screen, text window, and PM-can start a session for any other application type, including DOS Sessions. Applications running in DOS Sessions cannot start sessions.

DosStartSession can be used to start either a foreground or a background session, but a new session can be started in the foreground only when the session of the caller, or one of the descendant sessions of the caller, is currently executing in the foreground.

A session can be started as an unrelated session or as a child session.

In the following code fragment, an unrelated, foreground session is created, and the application, SIMON.EXE, is started in the new session:

 #define INCL_DOSPROCESS       /* Process and thread values */
 #define INCL_DOSSESMGR
 #include <os2.h>
 
 #define HF_STDOUT 1      /* Standard output handle */
 
 STARTDATA  sd;
 PID        pidProcess;
 CHAR       szBuf[CCHMAXPATH];
 ULONG      ulSessionID, cbWritten;
 APIRET     rc;
 CHAR       szPgmName[] = "SIMON.EXE";
 
 sd.Length = sizeof(sd);                    /* Length of the structure */
 sd.Related = SSF_RELATED_INDEPENDENT;      /* Unrelated session       */
 sd.FgBg = SSF_FGBG_FORE;                   /* In the foreground       */
 sd.TraceOpt = SSF_TRACEOPT_NONE;           /* No tracing              */
 sd.PgmTitle = (PSZ) NULL;                  /* Title is PgmName        */
 sd.PgmName = szPgmName;                    /* Address of szPgmName    */
 sd.PgmInputs = (PBYTE) NULL;               /* No command line args    */
 sd.TermQ = (PBYTE) NULL;                   /* No terminal queue       */
 sd.Environment = (PBYTE) NULL;             /* Inherits environment    */
 sd.InheritOpt = SSF_INHERTOPT_PARENT;      /* Uses parent environment */
 sd.SessionType = SSF_TYPE_PM;              /* PM session              */
 sd.IconFile = (PSZ) NULL;                  /* Uses default icon       */
 sd.PgmHandle = 0;                          /* Used by Win calls       */
 sd.PgmControl = SSF_CONTROL_MAXIMIZE;      /* Starts app maximized    */
 sd.InitXPos = 0;                           /* Lower left corner       */
 sd.InitYPos = 0;                           /* Lower left corner       */
 sd.InitXSize = 0;                          /* Ignored for maximized   */
 sd.InitYSize = 0;                          /* Ignored for maximized   */
 sd.ObjectBuffer = szBuf;                   /* Fail-name buffer        */
 sd.ObjectBuffLen = sizeof(szBuf);          /* Buffer length           */
 
 rc = DosStartSession(&sd, &ulSessionID, &pidProcess);
 
 if (rc) {
     DosBeep(750,250);
     DosWrite(HF_STDOUT, "error starting new session\r\n", 28, &cbWritten);
     DosExit(EXIT_PROCESS, rc);
 }

Before calling DosStartSession, you must create a STARTDATA data structure that defines the session to be started. Different lengths for the data structure are supported to provide compatibility and various levels of application control.

DosStartSession uses the STARTDATA structure to specify the details of the new session, such as the name of the application to start in the session, whether the new session should be started in the foreground or background, and whether the new session is unrelated or is a child session of the session calling DosStartSession.

When a session is created, the title specified in STARTDATA, (or the application title if no title is specified in STARTDATA) is added to the Window List.

The Related field in the STARTDATA structure specifies whether the new session is related to the session calling DosStartSession.

If the InheritOpt field in the STARTDATA data structure is set to 1, the new session inherits the environment and open file handles of the calling process. This applies for both unrelated and related sessions.

Controlling the Execution of Child Sessions

Once a process has started a child session, it can use DosSelectSession to control the child session.

A process calls DosSetSession to set the selectability and bonding of a child session.

Setting User Selectability of a Child Session

A process calls DosSetSession to set the selectability of a child session. When a child session is selectable, the user can select it from the Window List or by using Alt+Esc. When a child session is nonselectable, the user cannot select the session from the Window List or move to it by using the Alt+Esc keys.

In the following code fragment, DosSetSession makes a child session nonselectable:

 #define INCL_DOSPROCESS       /* Process and thread values */
 #define INCL_DOSSESMGR
 #include <os2.h>
 
 ULONG ulSessionID;
 STATUSDATA stsdata;
 
 stsdata.Length = sizeof(stsdata);
 stsdata.SelectInd = SET_SESSION_NON_SELECTABLE;  /* Non-selectable         */
 stsdata.BondInd = SET_SESSION_UNCHANGED;         /* Leaves session bonding */
                                                  /* index unchanged        */
 DosSetSession(ulSessionID, &stsdata);

Once a child session is made nonselectable, the user cannot select the session from the Window List or move to it by using the Alt+Esc keys. However, the parent session can still bring the child session to the foreground by using DosSelectSession. If the session contains a Presentation Manager application or is a windowed session, the user will still be able to select it with a mouse.

The parent session can make a nonselectable child session selectable by setting the SelectInd field to SET_SESSION_SELECTABLE in the STATUSDATA structure. DosSetSession can be called only by a parent session and only for a child session. That is, the calling process must be the process that started the child session using DosStartSession. Neither the parent session itself nor any grandchild, nor any other descendant session beyond a child session can be the target of this call.

Additionally, DosSetSession cannot be used to change the status of a session that was started as an unrelated session. The Related field in the STARTDATA structure must have been set to 1 when the session was started.

Binding Child Sessions to Parent Sessions

An application can use DosSetSession to establish a bond between a parent session and one of its child sessions. When the two sessions are bound, OS/2 brings the child session to the foreground when the user selects the parent session.

In the following code fragment, a parent session is bound to the child session specified by the ulSessionID parameter:

 #define INCL_DOSPROCESS       /* Process and thread values */
 #define INCL_DOSSESMGR
 #include <os2.h>
 
 ULONG ulSessionID;
 STATUSDATA stsdata;
 
 stsdata.Length = sizeof(stsdata);
 stsdata.SelectInd = SET_SESSION_UNCHANGED;   /* Leaves select setting alone */
 stsdata.BondInd = SET_SESSION_BOND;          /* Binds parent and child      */
 
 DosSetSession(ulSessionID, &stsdata);

When the application uses DosSetSession to establish a parent-child bond, any bond the parent has with another child session is broken. The application can remove the parent-child bond by calling DosSetSession with the BondInd field (in the STATUSDATA structure) set to SET_SESSION_NO_BOND.

A parent session can be executing in either the foreground or the background when it calls DosSetSession.

Switching a Session to the Foreground

An application can bring a session to the foreground, or select the session, by calling DosSelectSession.

DosSelectSession can only be used to select the current session or one of the current session's child sessions. It cannot be used to select a grandchild session, or any other descendant session beyond a child session, or any sessions that were started as unrelated sessions.

The session making the call, or one of its child sessions, must be executing in the foreground at the time the function is called. A process can call DosSelectSession with its own session identifier to switch itself to the foreground when one of its descendants is executing in the foreground.

The following code fragment uses DosSelectSession to switch the child session specified by the ulSessionID parameter to the foreground for five seconds. The application then switches the parent session back to the foreground:

 #define INCL_DOSPROCESS         /* Process and thread values   */
 #include <os2.h>
 
 ULONG ulSessionID;
 
 DosSelectSession(ulSessionID);   /* Switches child to foreground */
 DosSleep(5000);                  /* Sleeps for 5 seconds         */
 DosSelectSession(0);             /* Switches parent back         */

Terminating a Session

DosStopSession can be used by a parent session to stop one or all of its child sessions. If the child session specified in the call to DosStopSession has related sessions, the related sessions are also ended. The parent session can be running in the foreground or the background when it calls DosStopSession. If the child session is running in the foreground when it is ended, the parent session becomes the foreground session.

DosStopSession can only be called by a parent session for a child session. Neither the parent session itself, nor any grandchild, nor any other descendant session beyond a child session, nor any unrelated session, can be the target of this call.

In the following code fragment, the child session specified by the ulSessionID parameter is ended:

#define INCL_DOSPROCESS       /* Process and thread values */
#define INCL_DOSSESMGR
#include <os2.h>

ULONG ulSessionID;

DosStopSession(STOP_SESSION_SPECIFIED, ulSessionID);

An application can end all its child sessions by setting the first parameter to STOP_SESSION_ALL in the call to DosStopSession. If this is specified, the second parameter is ignored.

A process running in a child session can ignore the request to end. If the process has set up its own exception handler, it might not end immediately after the call to DosStopSession. The only way the parent process can be certain that the child session has ended is to wait for notification through the termination queue specified in the call to DosStartSession that started the session. When the child session ends, OS/2 writes a data element into the termination queue, specifying the child process identifier and the termination status.

If the process in the session specified by DosStopSession has not ended, then DosStopSession still returns a normal return code. You can ensure that a process in a session has ended by waiting for notification from the termination queue specified with DosStartSession.