March 94 - CONCURRENT PROGRAMMING WITH THE THREAD MANAGER
CONCURRENT PROGRAMMING WITH THE THREAD MANAGER
ERIC ANDERSON AND BRAD POST
Let us introduce you to the latest addition to the Macintosh Toolbox -- the Thread
Manager. The Thread Manager enables concurrent programming so that you can
provide true multitasking within your application. We give a quick overview of the
Thread Manager and then move on to discuss advanced programming techniques not
found in the Thread Manager documentation.
The Thread Manager is a new part of the Macintosh Toolbox that provides both
cooperative and preemptive threads of execution within an application. Although it's
available only within your application and is not used for systemwide preemption, you
can take advantage of it in many valuable ways:
• Use a threaded About box so that your application can continue running in
the background while displaying a modal dialog.
• Do anything you do today at idle time with null events within a thread.
This avoids the complexity of writing idleProcs.
• Decouple time-consuming processes from the user interface of your
application. For example, create an image-rendering thread for each image to
render, and use the main thread for the user interface of the application.
Similarly, with graphics screen redraws and print spooling, have a thread do
the redraw or spool a print job while the main thread handles the user
interface.
• Construct complex simulations without complex logic. For example, in a
program that simulates city streets, use a thread for each car, one for each
traffic signal, and one for the time of day.
• Use threaded I/O and communication to easily allow an application to act
as both a client and a server. With this approach, applications can handle
incoming questions and wait for incoming answers simultaneously. Examples
of this are discussed in more detail in "Threads on the Macintosh" indevelop
Issue 6.
The Thread Manager has all the rights and privileges of other services in the
Macintosh Toolbox, such as a trap interface to avoid linking a library into your code
and header files for C, Pascal, and assembly-language programmers. It's a fully
supported product and will be around for years to come. You can license the Thread
Manager through Apple's Software Licensing group (AppleLink SW.LICENSE). The
Thread Manager works on all Macintosh platforms running System 7 or later.
THREAD MANAGER OVERVIEW
This section describes the two types of threads -- cooperative and preemptive -- and
the basic services for creating, scheduling, and deleting threads and gathering thread
status. It also discusses the main thread, code organization, and thread disposal.
COOPERATIVE THREADS
Cooperative threads allow cooperative multitasking. They're the easiest to use in terms
of scheduling and accessibility to Toolbox traps. Everything you can do today in a
standard application you can do with a cooperative thread -- memory allocation, file
I/O, QuickDraw, and so on. Cooperative threads yield to other cooperative threads only
when the developer explicitly makes one of the Thread Manager yield calls or changes
the state of the current cooperative thread.
Cooperative threading in the Thread Manager is similar to the cooperative threading in
the Threads Package made available through APDA a few years ago (see "Threads on the
Macintosh" indevelop Issue 6). In fact, this library should no longer be used since the
Thread Manager is replacing it as the preferred method. Converting your applications
from the Threads Package to the Thread Manager is easy as long as you don't rely
heavily on the internal data structures provided by the Threads Package. The big
advantage to using the Thread Manager is that thread stacks are register swapped, not
block moved, during a context switch.
PREEMPTIVE THREADS
Preemptive threads allow true multitasking at the application level. Whenever the
application gets time from the Process Manager, preemptive threads for that
application are allowed to run. Unlike cooperative threads, which execute only when a
previously running cooperative thread explicitly allows it, preemptive threads may
interrupt the currently executing thread at any time to resume execution. You can
make the preemptive thread yield back to the just-preempted cooperative thread with
any of the Thread Manager yield calls. Alternatively, a preemptive thread can simply
wait for its time quantum to expire and automatically yield back to the cooperative
thread it interrupted. If the interrupted cooperative thread is in the stopped state, the
next available preemptive thread is made to run. Preemptive threads then preempt
each other, in a round-robin fashion, until the interrupted cooperative thread is made
ready. Figure 1 illustrates the default round-robin scheduling mechanism for threads.
For situations where you don't want a thread to be preempted, the Thread Manager
provides two calls for turning off preemption (see the next section). These calls don't
disable interrupts, just thread preemption.
Figure 1 Round-Robin Scheduling Mechanism Because a preemptive thread can
usually "interrupt" cooperative threads and doesn't need to explicitly use the API to
yield to other threads, preemptive threads have to conform to the guidelines set up for
code executed at interrupt time. Don't make any calls that are listed inInside Macintosh
X- Ref , Revised Edition, Appendix A, "Routines That Should Not Be Called From Within
an Interrupt." No moving of memory is allowed and definitely no QuickDraw calls
should be made. QuickDraw calls may seem tempting and may even appear to work, but
they fail on many occasions and can corrupt other parts of memory in subtle ways that
are very difficult to debug. (QuickDraw GX was designed to be reentrant, so
preemptive threads may make use of the QuickDraw GX feature set if they follow the
rules set up by QuickDraw GX.) If there's only one thing you learn in this article,
make sure it's this:preemptive threads must follow interrupt-time rules!
THE THREAD MANAGER API
There are several data types declared in the Thread Manager API that determine the
characteristics of a thread. These include the type of thread you're dealing with
(cooperative or preemptive), the state of a thread (running, ready, or stopped), and a
set of options for creating new threads. With the thread creation options, you can
create a thread in the stopped state (to prevent threads from beginning execution
before you want them to), create a thread from a preallocated pool of threads (which is
how you would create a new thread from a preemptive thread), or tell the scheduler
not to save FPU state for the new thread (to reduce scheduling times for threads that
don't use the FPU). These creation options are combined into one parameter and
passed to the routines that create new threads.
General-purpose services make it possible to create a pool of preallocated threads and
determine the number of free threads in the pool, get information on default thread
stack sizes and the stack currently used by a thread, and create and delete threads.
There are basic scheduling routines for determining the currently running thread and
for yielding to other threads. You can also use the preemptive scheduling routines --
ThreadBeginCritical and ThreadEndCritical -- to define critical sections of code. A
critical section of code is a piece of code that should not be interrupted by preemptive
threads -- because it's changing shared data, for example. The use of critical sections
of code is needed to prevent interference from other threads and to ensure data
coherency within your code. Note that preemptive threads run at the same hardware
interrupt level as a normal application, andcalls to ThreadBeginCritical don't disable
hardware interrupts; they simply disable thread preemption .
Advanced scheduling routines enable you to yield to specific threads and to get or set
the state of nearly any thread. (You can't set a thread to the running state or change the
state of the currently running thread if it's in a critical section of code.) Custom
context-switching routines allow you to add to the default thread context and may be
associated with any thread on a switch-in and/or switch- out basis. Any thread may
have a custom context switcher for when it gets switched in and a different switcher
for when it gets switched out. In addition, a custom scheduling procedure may be
defined that gets called every time thread rescheduling occurs. All of these features
may be used by both types of threads.
The Thread Manager also provides debugging support: a program or debugger can
register callback routines with the Thread Manager so that it gets notified when a
thread is created, deleted, or rescheduled.
THE MAIN THREAD
When an application launches with the Thread Manager installed, the Thread Manager
automatically defines the application entry point as the main cooperative thread. The
main cooperative thread is commonly referred to as theapplication thread or main
thread . There are several guidelines you should follow regarding the main thread.
These aren't hard and fast rules, but just guidelines to keep you from having debugging
nightmares. Remember that cooperative threads must make explicit use of the Thread
Manager API to give time (yield) to other cooperative threads.
First, the Thread Manager assumes the main thread is the thread that handles the event
mechanism (WaitNextEvent, GetNextEvent, ModalDialog, and so on). Events that can
cause your application to quit should be handled by the main thread: the application
was entered through the main thread andshould exit through it as well. The Thread
Manager schedules the main thread whenever a generic yield call is made and an OS
event (such as a key press or mouse click) is pending. This is done to speed up
user-event handling; otherwise, it could take a long time before the main thread got
time to run, because of the round-robin scheduling mechanism used for cooperative
threads. In general, all event handling should be done from the main thread to prevent
event-handling confusion. For example, if the main thread calls WaitNextEvent while
another thread is calling GetNextEvent while yet another thread is calling ModalDialog,
and they're all yielding to each other, sorting out which event belongs to which thread
becomes a nightmare. Just let the main thread handle all events. Your application will
run faster and work better in the long run.
The second guideline is to avoid putting the main thread in a stopped state. Doing so is
perfectly legal but can lead to some exciting debugging if your application makes
incorrect assumptions about the state of the main thread.
Last but not least, be sure you call MaxApplZone from the main thread to fully extend
your application heap before any other cooperative threads get a chance to allocate, or
move, memory. This requirement is due to the limitation on cooperative threads
extending the application heap -- they can't.
DISPOSING OF THREADS
When threads finish executing their code, the Thread Manager calls DisposeThread.
Preemptive threads are recycled to avoid moving memory. Recycling a thread causes
the Thread Manager to clear out the internal data structure, reset it, and place the
thread in the proper thread pool. (There are two pools, one for cooperative and one for
preemptive threads.) Since recycling a thread doesn't move memory, preemptive
threads can dispose of themselves and other threads safely.
The Thread Manager handles disposing of threads automatically, so it's not necessary
for the last line of your code to call DisposeThread unless you want to -- for example,
to recycle all terminating cooperative threads. DisposeThread takes three parameters:
the thread ID of the thread to be disposed of, a return value to return to the creator of
the thread, and a Boolean flag that tells the Thread Manager whether or not to recycle
the thread. A preemptive thread should only call DisposeThread with the recycle flag
set to true.
The Thread Manager API also defines a routine, SetThreadTerminator, that allows your
application to install a callback routine to be called when a thread terminates --
which is when it's disposed of. You could use SetThreadTerminator to do any cleanup
needed by the application, but remember, you can't move memory during termination
of a preemptive thread because you're still in the preemptive state.
When your application terminates, threads are terminated as follows:
• Stopped and ready threads are terminated first but in no special order.
• The running thread, which should be the main thread, is always
terminated last.
CODE ORGANIZATION
To use the Thread Manager most effectively, break your applications into discrete
functional pieces, similar to the code organization used for object-oriented
applications. Keep the code for event handling, window management, calculation, and
drawing separate. In general, design your code such that the discrete parts may run
independently of each other, but in an organized and coherent way. Object-oriented
code, in general, is designed this way and is much easier to think about in a concurrent
manner. The following sections discuss techniques that you can use to write more
effective thread code. The examples illustrate what we learned from lots of trial and
error during the development of the Thread Manager, which should reduce the
development time for your threaded applications.