OS/2 Threads Cookbook

From OS2World.Com Wiki
Revision as of 23:59, 14 July 2018 by Martini (talk | contribs)
Jump to navigation Jump to search

By Stephen Best

Binary Samples at COOKBO.ZIP

Version 1.2

Stephen Best
P.O. Box 3097
Manuka A.C.T. 2603
Australia

Copyright (c) 1991, 1992 Stephen Best

This document is an attempt to collect together and share a number of my observations and ideas about programming for OS/2 Presentation Manager using multiple threads that have evolved over time and been gleaned (gratefully) from other explorers in this area. A thorough understanding of the use of threads is essential for construction of all but the most trivial Presentation Manager programs and it is hoped that the ideas contained herein with be of aid to programmers beginning to tap into the exciting possibilities that the use of multiple threads introduce.

If you would like the full C source for the examples discussed herein, please contact me via FidoNet/CompuServe or at the address given with your Mastercard/Visa particulars. The cost is $A45 (approx. $US34) with free transfer via CompuServe. An additional $A10 will be charged for postal delivery if required (3.5 inch media only). Payment entitles the licensee to use the source from the examples in any programs of their own.

Also, if you have any comments at all regarding the material contained herein, including errors and omissions, I would be more than happy to hear of them.

Stephen Best

28 February, 1992

Introduction

OS/2 as a single user system has the potential to substantially change the user's perception as to how a personal computer should work. Programs using multiple threads can not only increase execution performance (both perceived and actual) but also change the emphasis in user-application interaction to one where the user has more control and flexibility and where the application itself takes on a passive role. The program should always be receptive to interaction with the user even if this is just the capability for that user to change his/her mind after initiating a lengthy activity.

Users that repeatedly tell you that "they don't need to multitask" will have great difficulty in reverting to single threaded software after having had the luxury of using a well designed and responsive multi-threaded application. Thus anyone wishing to compete in the market may have a hard time selling their product in an increasingly aware public arena. It is also hoped that all programmers will want to wring the maximum result from an environment for their efforts, and I think multiple threads have the potential for good returns in this area.

It is also true, though not universally appreciated, that programming to the multi-threaded model has significant impact on the overall program design, and it is important to have this in mind up front to avoid major restructuring of the program at a later stage.

This document is aimed at the OS/2 programmer wishing to tap into the power that programming with multiple threads provides. As such, I will attempt to cover all the essential issues related to threads, a guide to where and when I think threads are applicable and some substantial coding examples that I think demonstrate this. These examples are in C and are for OS/2 2.x, though conversion to other languages and/or OS/2 1.x should not be too difficult once the concepts are understood.

All code has been tested with IBM OS/2 2.0 pre-release level 6.177 on an IBM PS/2 Model 80. The IBM C Set/2 compiler, linker and 6.177 toolkit headers were used.

It is my belief that practically ALL programs for OS/2 Presentation Manager will benefit from using multiple threads in their design, and indeed have a responsibility to do so given the message switching architecture of PM. Comments (especially those from sources with a vested interest in promoting second rate software products) that there is only a minimal requirement for multi-threaded program design should be considered in the light of the immediate and obvious benefits that their proper use can achieve.

Polemics over, let's learn about OS/2 threads.

What is a thread?

The thread is the basic level of execution under OS/2 and is roughly equivalent to the task of other systems. A program (or process) has a single thread at the beginning of its execution and can optionally split the activity of that program over a number of threads. Each thread of execution will be time-sliced on the processor (CPU) of the computer together with other threads of that application, and those of other applications active concurrently. A priority mechanism exists to ensure that the thread with the highest priority is always active, with control passing to other threads of a lower priority when the higher 'blocks' or is waiting on an event. On top of this OS/2 has a sophisticated scheduler to dynamically alter thread priority to achieve responsive overall performance or multitasking within the system.

Note that splitting a single processor intensive task over a number of threads does not in itself achieve anything as the processor itself is a finite resource which cannot be driven beyond its capacity. Indeed the housekeeping in alternately dispatching threads may slow down execution in this case. (It may be worthwhile though to keep in mind that a future version of OS/2 may well support multiple physical processors, and the requirement for dividing compute bound tasks will change in this case).

The criteria for dividing a process into threads as discussed herein is aimed at isolating the activity of a program by either priority, functional units or access to a resource.

Consider an application which presents the user with a number of child windows or 'views', of which only one (the active window) can receive the keyboard focus at a time. It would in this case make sense to give the active/focus window a higher priority than the others, if concurrent activity in other windows is likely to impede the responsiveness of the one with which the user is interacting at that time. This can be achieved quite easily by assigning each window its own 'worker' thread and setting the priority of the active/focus window thread higher than that of its siblings. In this case the thread for the active window will receive the processor resource that it requires without interference from the other windows, aiding in the perceived responsiveness of the application.

Actual overall efficiency can be achieved by overlapping processor intensive tasks with those for input/output eg. disk I/O. OS/2, as a true pre-emptive multitasking system, can balance the priorities between processes to maximize throughput but it is the application's responsibility to separate within itself lengthy I/O tasks from processor intensive ones, and especially those likely to interfere with servicing of the system message queue (more on this important area later). For example, if a user initiates a lengthy file open/save operation or printing activity it should be possible to interrupt this activity if the user changes his/her mind, or still interact with other facets of the application in parallel with the I/O activity. Failure to split this activity off from the primary thread can even inhibit the user's ability to switch to another unrelated application on the desktop. In this case, it may be advantageous to spawn a thread specifically for servicing the disk or printer asynchronously. The main thread could then off-load such tasks and 'queue' them to a background thread, and get on business of interacting with the user. Note that in this case it makes no sense to have a number of threads for a single resource (like a printer) as no efficiency is gained.

It may be helpful to think of a one for one correspondence between threads and 'resources' (be they windows or the disk or a printer), with a 'master' thread interacting with the user (and hence the system message queue). It is this concept of resource based threads that will be expounded upon in the following.

Message queues

Presentation Manager (among other GUI systems) has a message switching architecture to facilitate the routing of messages of different types among the 'windows' that make up the presentation layer for OS/2. An application can receive messages from the system eg. when a user attempts to re-size a window, or can send messages to itself or other windows in the system. Messages can either be SENT (explicitly with WinSendMsg or implicitly with a large number of other API calls eg. WinSetWindowText) or POSTed (with WinPostMsg). Sent messages (and those API calls that result in sent messages) will be turned into direct calls to the window procedure for the window specified in the send. Posted messages on the other hand will be queued in the application's message queue for deferred execution.

The application message queue is created by the application itself with WinCreateMsgQueue and it is the act of doing so that distinguishes that application as a Presentation Manager one (as opposed to a character mode application executing in its own session). An application may create as many message queues as desired provided that only one message queue exists for each thread. Message queues other than the primary one are optional in multi-threaded applications and the following examples will attempt to demonstrate where multiple application message queues might be applicable.

Messages queued on a message queue (by the system or the application itself) are un-queued with (generally) WinGetMsg in a message loop and then dispatched to the appropriate window procedure with WinDispatchMsg. This WinDispatchMsg can be thought of as turning the POSTed message into a SEND for immediate execution. In both cases, the window handle given specifies the appropriate window procedure for that message ... the association between window handle and procedure (for other than pre-registered classes) is made by the application with the combination of WinRegisterClass and WinCreate(Std)Window.

The system message queue (of which there is only one for the whole Presentation Manager session) is provided to queue those 'messages' that will be subsequently distributed to the appropriate application message queue(s) at such time that the context of the message can be determined. The primary consideration here is user input (both keyboard and mouse actions) that may occur asynchronously to the application message flow. The 'problem' for PM programs is that the application processing of any message can itself change the destination for keyboard and mouse messages pending in the system message queue (eg. explicitly with calls WinSetFocus or WinSetCapture) and thus it is only when PM itself regains control from prior messages that it is possible to determine the appropriate application queue in which to place the keyboard or mouse message. In addition, the program is responsible for processing messages dealing with loss of focus and activation before other windows can be activated. The implication for a PM program is that it should always be available for processing user input events, and process all incoming messages quickly.

The methodologies discussed in this document are aimed at off-loading the bulk of the processing requirement for the application from the 'input' message thread to other 'non-input' threads, making the application always receptive to user input, and thus increasing the responsiveness of the application and the system as a whole. It may be helpful to consider that serialization of keyboard and mouse messages in the system queue is not so much a 'problem' to be overcome with adding threads, but that the main 'input' thread of the application is just a vehicle for receiving input from the system and like all shared resources, to be treated accordingly.

Performance and restrictions

Sent messages can be processed faster than posted messages because they never appear in the message queue of the application and thus avoid the message loop altogether. The throughput of inter thread posts will be slower still. This is not to say that posts should be avoided, but that it may be desirable to use sends rather than posts when an clear choice exists between the two. Sends also have the guarantee that any dynamic memory area addressed by the message parameter(s) will remain current for the life of the send, which is a benefit if more data than the eight bytes permitted with the two 32 bit message parameters themselves is required. As a bonus, the return code from the receiving window procedure method is available upon completion of the send. Sends (because they are translated into calls to the window procedure) will cause the window procedure(s) to be called recursively, and thus may place excessive demands on the program stack with high levels of recursion.

Posts on the other hand, because of their asynchronous nature will be serialized in the message queue and processed when the application itself enters message loop processing. This means that any dynamic data addressed by message parameters when the post was issued may no longer be valid. This consideration requires a number of differing techniques to transfer more data than the message parameters themselves permit. Another important point to note about posts is that the message may not actually be posted should the message queue be full at the time of the post. The return code from WinPostMsg should thus be checked to see if the post was in fact accepted and implementing a delayed retry or some pacing algorithm to ensure the message is not lost. Despite the above, posts will play a big part in the interaction of and communication between threads and thus the techniques for achieving efficient and reliable use of them is presented herein.

Another difference between sends and posts is the context in which it is valid to issue them. Posts can be issued without restriction between threads and will appear in the message queue of the thread with which the window addressed (by the window handle specified) was created. A variation on WinPostMsg is WinPostQueueMsg where the handle of the message queue itself is specified instead of the window handle. This permits an application to queue messages to another thread (assuming the receiving thread has created its own message queue) when no actual window procedure may exist for that thread. This variation will be explored in one of the following examples.

Sends on the other hand can only be issued between threads each having a message queue, and for reasons following should be avoided for anything other than intra thread communications. Firstly, sends to a window created on a different thread than that from which the send is issued will still execute in the context of that window's thread and thus may incur a performance penalty due to the overhead involved in the required thread switch. In addition, inter thread sends (and API calls that result in sends to other threads) may result in a deadlock situation should the receiving thread be waiting (using say a semaphore) on some event from the calling thread at the time the send is issued. (Note that WinMsgMuxSemWait exists specifically to avoid this deadlock situation.) The temptation may be to think that creating windows each on separate threads will permit extensive processing without interference with the overall message flow, but it must be remembered that all threads that create a (non object) window are subject to the same input restrictions discussed above. It is because of these reasons that I propose creation of all windows on the initial thread and exclusive use of posts for inter thread communications in this document.

The above brings up the important concept of distribution of responsibilities within the application. The model I use and propound herein is that the main (initial) thread be used almost exclusively for window 'management'. Thus ALL (non object) windows will be created (or 'owned') by this thread and any activity likely to involve more than minimal processing off-loaded to non-window 'service' threads. The main thread (simply because of the fact that this is where the windows were created) will be the sole 'input' thread subject to the keyboard/mouse message restrictions discussed above. All other threads can thus undertake substantial processing tasks (or waits) without impacting the application's ability to appear responsive to user interaction. Using this demarcation of processing responsibility, it is unlikely that the problem of using inter thread sends will arise.

Managing threads

Threads (over and above the initial one allocated when the program begins execution) are created explicitly with DosCreateThread. Each thread will have its own stack (allocated and committed dynamically with 2.x) but share all code and data areas of the parent process. Optionally a 32 bit parameter can be passed to the thread at this time and this is normally used to address thread initialization data (or 'thread parameters'). A thread so created will exist for the life of the program execution (process) unless it terminates itself by 'returning' or making a call to _endthread or DosExit (with EXIT_THREAD).

Due to any overhead in creating/destroying a thread it is normal to have the thread life tied to the 'owning' window or dialog box or failing that, the entire process. There are no rules as to how many threads should be created in the 'average' program as this will be governed by the activity and resource requirements of each. One way of deciding the number (and more importantly, function) of threads to create is to consider how many of the elements of the program you would like to run in parallel. Thus a program which creates a number of windows (all of which require extensive graphics) plus provides for background printing may create a thread for each window, with another for servicing the printer queue. Or maybe all the windows could share a single drawing thread if the processing requirements are smaller. The final consideration of thread numbers and function will depend on both the degree of interactivity and visible feel the programmer wishes to create with the program and how logically functions are isolated internally in the program itself. (Realistically, the same end result may be achieved by creating only a single 'service' thread in addition to the main thread and alternately allocating time to the respective resources, but maintaining the desired balance may require duplicating the function of the OS/2 scheduler itself, and hence be self defeating.)

A number of other API calls are related to threads. DosWaitThread (new with 2.x) allows the thread 'owner' (actually any thread) to wait until the specified thread is terminated and thus can be used when the owner itself is being destroyed for clean-up operations. DosSuspendThread and DosResumeThread allow another thread to temporarily halt execution of the specified thread, and resume operation at a later time. Due to the fact that it will probably not be possible to predict the exact stage of operation of the specified thread, these calls may not prove to be that useful, and indeed a similar effect can be achieved by resetting that thread's priority. DosSetPriority can be used to modify a thread's priority, or to place the thread in a different dispatching class. DosKillThread (also new with 2.x) can be used to terminate secondary threads but at the risk of leaving allocated resources used by that thread. DosEnterCritSec and DosExitCritSec can be used to temporarily disallow execution of all other threads in the process when serialized access to a resource of some type must be guaranteed, and using mutex semaphores is not appropriate. Finally, DosSleep can be used by a thread to surrender the remainder of its dispatching time slice or to delay execution for a specified amount of time.

DosCreateThread vs. _beginthread

No paper on OS/2 threads programming would be complete without a discussion on the differences between the use of the API function DosCreateThread and the replacement C compiler run-time extension _beginthread.

The problem with using DosCreateThread in a C program is that a number of C run-time library and inline functions assume a single instance of internal static variables and the behaviour of the program may be undefined when this common data is accessed by two or more threads concurrently. Such functions include malloc/free, strtok and rand. The standard malloc/free functions, for example, assume unrestricted access to the heap management control information and corruption may occur if access to this information is preempted by a second thread requesting access to the same data. The strtok and rand functions both save their current state between calls which may result in indeterministic behaviour due to dynamics in access of threads to the previous state.

The solution adopted by a number of vendors of C compilers has been to prevent these undesirable effects by either serializing access to such data that must be shared, or providing an individual instance of the data for each thread created. This is achieved firstly by performing some run- time initialization of localized thread variables with _beginthread prior to invoking the DosCreateThread function. Secondly, a number of run-time functions are modified to either access these local variables or request serialization (with DosRequestMutexSem or DosEnterCritSec) when the data must be shared. To the programmer, such management is transparent provided that the _beginthread function is used exclusively and the program is linked with the appropriate multi-threading run-time library.

An alternative solution to the above approach is to restrict a program's use of functions to those documented to be reentrant. True reentrant routines will use a stack-based local copy of any data (where required) and thus avoid any contention from other threads as each has its own individual stack. The IBM C Set/2 Subsystem run-time library (with the heap management functions replaced with use of OS/2 suballocation routines) may well support this alternative. Such may be desired to minimize the run-time overhead in providing contention support when none is desired.

Both examples below use _beginthread for creation of threads and are compiled with the multi-threading switch and linked with the supporting run-time library.

Window data

Each window procedure associated with a window class will have some data to be retained over the life of the window, or between processing of messages. This 'static' data can be initialized when the window procedure receives its WM_CREATE or WM_INITDLG message and updated depending on subsequent message flow. It is common practise to place such 'static' data in a dynamically allocated area of memory and have this addressed by a window 'pointer'. Thus an area of the appropriate size will be allocated (with malloc) when the window is created and the address of this area saved in a window 'word' with WinSetWindowPtr. The address of this area will be retrieved with WinQueryWindowPtr immediately prior to processing of all other messages for the window, and the memory area disposed of (with free) in WM_DESTROY processing. Thus if multiple 'instances' of the window are created, each window can be assured of integrity of its own data. This can have an added benefit in reducing the total EXE file size, and more importantly promotes what I believe to be a good 'object oriented' programming style. Though not directly related to using threads, the concept of data encapsulation will be rigidly exploited in the coding examples contained herein.

Example 1

The first example below is the complete window procedure for a file search dialog. This dialog provides the user with a means to search a number of disks for a specified file, or ones matching the given 'mask' criteria. The user enters the desired file name (with or without free characters), selects a number of disks and presses the 'start' button. Once the search is initiated, the 'start' button changes its function to 'stop' to enable the user to interrupt the active search. As files are found that match the search criteria, they will be added to a list box which can be scrolled and an entry selected even though the search is still active, enabling the user to exit with the selected file without waiting for the search to complete. The 'stop' button reverts to its 'start' function when the search is complete. Whilst this search is in progress, the user can move the dialog window or interact with other applications on the desktop.

The virtue of using a separate thread for this type of dialog is that the I/O intensive logic for scanning the directory list(s) for the specified files can be segregated from that of interacting with the user. The end result is that maximum flexibility of interaction is achieved without impacting the speed of the actual search.

This dialog window procedure creates the search thread in the WM_INITDLG processing and terminates the thread in WM_DESTROY, thus the thread exists for the life of the dialog session. The search thread issues a mux wait on two event semaphores: a 'trigger' to initiate a new search and a 'terminate' event to signal thread termination. Once the search is active, it can be interrupted by setting the fInterrupt flag TRUE, and this flag is checked periodically in the search process.

As files are found that match the specified criteria, the search thread posts a UM_SEARCHUPDATE message to the 'owning' thread to signal that the found entry should be added to the list box. In this case, we cannot use the message parameters on the post to fully contain the data to be transferred as the file name length clearly exceeds the eight bytes available. What has been done in this example is to use a simplified form of circular buffer, with an 'in' and 'out' count. Thus entries can be added to the buffer when the 'in' count does not exceed the 'out' count by the total number of entries in the buffer, otherwise we would overlay data that had not been accepted by the owning thread. As the buffer and counters are accessible by both threads, all that is required is to signal the owning thread that new data has been added to the list and should be processed. This is done here by equating UM_SEARCHUPDATE to WM_SEM2 and using message parameter 1 as a progress flag, with TRUE indicating completion of the search. The WM_SEM1-4 messages are special in that the messages are not stacked in the message queue, but accumulated into one message with the message parameter 1 seen by the recipient being the OR'ed result from all the messages parameters posted. WM_SEM2 (rather than WM_SEM1) was selected as the priority of this message is lower than that of keyboard/mouse messages thus avoiding any impact on user interaction whilst transferring data. (If you move the mouse pointer around rapidly you will notice that the search will slow down.)

A few other observations on this example. Because of the nature of the WM_SEMx messages, there is no risk of flooding the application message queue (and hence losing a post) in that there can be only one message of this type in the queue at any time. Also, it is likely that a number of found entries can be transferred for each post the main thread sees, hence improving the efficiency of the transfer. If the circular buffer is full (indicated by the value of the difference in the counters) the search thread issues DosSleep to surrender the remainder of its dispatching time slice and thus allowing the main thread to process the queued entries and free up the slots required.

Another important element is that the dialog window procedure has been structured to not have to depend synchronously on the action of the search thread, allowing the search to be interrupted and end without the main thread logic having to issue a wait. If it is possible to avoid such waits, an extra level of semaphore handshaking can be omitted.

Example 2

The second example is a window procedure (together with its 'service' thread) for utilizing 'shadow' bitmaps to facilitate fast paints and to off-load the bulk of the processing requirement to a 'non input' thread. A shadow bitmap (as used in this example) is the context for the drawing operations which can proceed offline from the main window procedure and be quickly transferred to the window context with GpiBitBlt in the WM_PAINT method. This implementation is ideal when an application can present the completed drawing, rather than show the drawing activity in progress. Also, if the destination window is to be restored (eg. after being covered by another) a subsequent call to the processor intensive graphics functions is avoided.

This example differs from the first in that the service thread allocates its own message queue, and communications between threads is achieved with posts (rather than semaphores). Thus, a request for some activity can be 'queued' to the service thread (with WinPostQueueMsg) by specifying the handle of the message queue itself. Note that WinPostMsg could not be used in this case as the service thread has not actually created any windows and hence no window handle exists to enable PM to determine which queue is applicable. The service thread has its own message loop to un-queue the posted requests and route to the appropriate logic based on message ID, and in this sense is no different from a normal window procedure. When the activity is complete, the service thread posts a completion message to the 'owning' thread to trigger the appropriate action (eg. paint). Lastly, the service thread is terminated by posting WM_QUIT to its message queue which causes the loop to terminate.

The service thread in this example exists for the life of its 'owning' window, created in WM_CREATE and terminated in WM_DESTROY. As the main procedure must insure that the service thread's message queue is valid, a semaphore is set by the service thread when the queue handle is available to its owner.

If multiple instances of this window are required, each will have its own service thread and this enables a priority mechanism to exist to ensure that the active window will be drawn before other, non-active windows. This is achieved in this example buy raising or lowering the service thread priority (in WM_ACTIVATE) so that the active window's priority is always higher that its siblings. The priority mechanism is absolute in that the service thread for the active window must 'block' (in WinGetMsg) before the other windows will receive any processor resource. Note that as implemented in this example this set priority will still be lower than that of the main 'input' thread to reduce any interference with desktop operations.

When using this message queue technique, it is possible to optionally check for pending messages posted in the queue with a call to WinQueryQueueStatus. In this example, as all output posts from the service thread are the same, some processing may be saved if processing of the current message is aborted in favour of pending messages of the same type. This should only be attempted when it can be quaranteed that the sequence of incoming messages is not disturbed.

This example has been structured so that the main window thread never has to explicitly wait for completion of a posted task (other than for thread termination and recovery from failed posts). If serialization is necessary, semaphores 'posted' by the service thread can be used to delay execution until desired. Alternatively, the main thread can wait for a posted completion message by using WinGetMsg and specifying the message identity. In the example given

WinGetMsg (pw->hab, &qmsg, (HWND) hwnd,
           UM_WINDOWUPDATE, UM_WINDOWUPDATE);

would delay the main thread until the requested service was complete. Note that either of the above (using semaphores or waiting for completion messages) issued from the main input thread will have the effect of stopping flow in the main message queue of the program, and delay incoming keyboard and mouse messages system-wide (as discussed above). The goal should thus be to structure the program so that such serialized dependencies are minimized (or ideally avoided).

Other possibilities

The above two examples represent a sample of the possibilities of managing program activity with multiple threads. A number of other methodologies exist which may prove applicable to different program requirements.

A variation on the shadow bitmap example above is to give drawing control of the window presentation space to a service thread. This has the similar benefit in that processor intensive graphics functions can be off-line from the main input thread with the bonus that the application user can see the drawing in progress, rather than wait for the shadow bitmap to be completed. To do this the program would (probably in WM_CREATE) associate a presentation space to the window context with WinOpenWindowDC and GpiCreatePS and pass this presentation space handle to the drawing thread. The drawing thread would thus receive requests from the main thread and invoke the graphics functions required to draw directly upon the window presentation space. Some provision may need to be made for retaining the results of the drawing activity should a full or partial re-paint be required due to window sizing or restoral.

An extension of using threads with their own message queue is to create object windows (windows created with a parent of HWND_OBJECT). Activity in such 'windows' is initiated with WinPostMsg as the object window handle is specified to identify the appropriate message queue and window procedure for that window. In all other respects, this is identical to the message queue example above. The use of object windows may be applicable when a thread exists to support a number of resources and no overlap in processing is required.

Conclusion

It is hoped that by now the reader has understood the fundamentals of why multiple threads are applicable to OS/2 Presentation Manager programs for improving the overall responsiveness of the desktop dictated by the message queue architecture, and the implications for presentation of a flexible user-application interface. The existence of threads in OS/2 provides the application designer with a rich set of techniques to distribute function within the program itself and co-ordinate activity. The goal of the application designer should be to identify opportunities for parallel operation, and to build the program with the appropriate threads to achieve this, whilst allowing the user to interrupt or abort any lengthy activity in progress.

Multiple threads, I feel, offer the means to totally transform a user's expectation of how personal computer software should work and hopefully this document will help bring about this new age of more responsive and flexible software.

References

The following references may be useful in expanding the reader's understanding of OS/2 multi-threading techniques and possibilities as applicable to Presentation Manager programming:

Utilizing OS/2 Multithread Techniques in Presentation Manager Applications, Charles Petzold
Microsoft Systems Journal Vol. 3 No. 2
Planning and Writing a Multithreaded OS/2 Program with Microsoft C, Richard Hale Shaw
Microsoft Systems Journal Vol. 4 No. 2
OS/2 PM Programming: A Performance Guide, P.G. Toghill
IBM Personal Systems Developer, Winter 1991
A Multithread CPU Monitor, Marc Cohen
OS/2 Notebook, The Best of the IBM Personal Systems Developer, Microsoft Press
Programming for Multithreaded Drawing, Charles Petzold
PC Magazine, Vol. 9 Nos. 10-12
Programming the OS/2 Presentation Manager, Charles Petzold
Microsoft Press
Inside OS/2, Gordon Letwin
Microsoft Press
Microsoft OS/2 Programmer's Reference Vol. 1
Microsoft Press
Programming Guide
IBM OS/2 Programming Tools and Information, Version 1.2
The Design of OS/2, H.M. Deitel and M.S. Kogan
Addison-Wesley
IBM C Set/2 User's Guide
IBM Publication number S10G-4444-0