March 93 - ASYNCHRONOUS ROUTINES ON THE MACINTOSH
ASYNCHRONOUS ROUTINES ON THE MACINTOSH
JIM LUTHER
The Macintosh has always supported asynchronous calls to many parts of its operating
system. This article expands on the information found in Inside Macintosh by telling
when, why, and how you should use functions asynchronously on the Macintosh. It
includes debugging hints and solutions to problems commonly encountered when
asynchronous calls are used.
When calling a routine synchronously, your program passes control to the routine and
doesn't continue execution until the routine's work has completed (either successfully
or unsuccessfully). This would be like giving someone a task and then watching them
perform that task. Although the task is eventually completed, you don't get anything
done while you watch.
On the other hand, when calling a routine asynchronously, your program passes
control to the routine, and the program's request is placed in a queue or, if the queue is
empty, executed immediately; in either case, control returns to the program very
quickly, even if the request can't be executed until later. The system processes any
queued requests while your program is free to continue execution, then interrupts you
later when the request is completed. This is like giving someone a task and going back
to your work while they finish the task. In most cases, it results in more work being
accomplished during the same period of time. Figure 1 illustrates the difference
between synchronous and asynchronous calls.
One situation in which you shouldn't use synchronous calls is when you don't know
how long it may take for the operation to complete, as with the PPC Toolbox's
PPCInform function, for example. PPCInform won't complete until another program
attempts to start a session with your program. This could happen immediately, but
the chances are far greater that it won't. If PPCInform is called synchronously, it
appears that the system has locked up because the user won't get control back until the
call completes. If you call PPCInform asynchronously, it doesn't matter if the function
doesn't complete for minutes, hours, or even days -- your program (and the rest of
the system) can continue normally.
Figure 1How Synchronous and Asynchronous Calls Work
You should also avoid synchronous calls when you can't know the state of the service
you've asked for. Program code that's part of a completion routine, VBL task, Time
Manager task, Deferred Task Manager task, or interrupt handler is executed at what's
commonly calledinterrupt time. Synchronous calls made at interrupt time often result
in deadlock. (See "Deadlock.") An asynchronous call can solve the problem: if the
service you call is busy handling another request, your asynchronous request is
queued and your program code can give up control (that is, the completion routine or
task your code is part of can end), letting the service complete the current request and
eventually process your request.
Routines called synchronously are allowed to move memory, while routines called
asynchronously purposely avoid moving memory so that they can be called at interrupt
time. For example, the File Manager's PBHOpen routine may move memory when
called synchronously, but won't when called asynchronously. If your code is executing
in an environment where memory can't be moved (for example, at interrupt time),
you must call routines asynchronously to ensure that they don't move memory.
At this time, the various lists inInside Macintoshof "Routines That May Move or Purge
Memory," "Routines and Their Memory Behavior," and "Routines That Should Not Be
Called From Within an Interrupt" are either incomplete or incorrect and can't be
trusted entirely. The reasons why a system routine can't be called at interrupt time
include: the routine may move memory; the routine may cause a deadlock condition; the
routine is not reentrant. This article shows how to postpone most system calls until a
safe time. You're encouraged to call as few system routines at interrupt time as
possible.
The routines discussed in this article are low-level calls to the File Manager, the
Device Manager (including AppleTalk driver, Serial Driver, and disk driver calls),
and the PPC Toolbox. All these routines take the following form:
FUNCTION SomeFunction (pbPtr: aParamBlockPtr; async: BOOLEAN): OSErr;
Routines of this form are executed synchronously when async = FALSE or
asynchronously when async = TRUE.
DETERMINING ASYNCHRONOUS CALL COMPLETIONYour program can use two
methods to determine when an asynchronous call has completed: periodically poll for
completion (check the ioResult field of the parameter block passed to the function) or
use a completion routine. Both methods enable your program to continue with other
operations while waiting for an asynchronous call to complete.
POLLING FOR COMPLETIONPolling for completion is a simple method to use when
you have only one or two asynchronous calls outstanding at a time. It's like giving
someone a task and calling them periodically to see if they've completed it. When your
program fills in the parameter block to pass to the function, it sets the ioCompletion
field to NIL, indicating that there's no completion routine. Then, after calling the
function asynchronously, your program only needs to poll the value of the ioResult
field of the parameter block passed to the function and wait for it to change:
• A positive value indicates the call is either still queued or in the process
of executing.
• A value less than or equal to 0 (noErr) indicates the call has completed
(either with or without an error condition).
Polling is usually straightforward and simple to implement, which makes the code
used to implement polling easy to debug. The following code shows an asynchronous
PPCInform call and how to poll for its completion:
PROCEDURE MyPPCInform;
VAR
err: OSErr;{ Error conditions are ignored in this procedure }
{ because they are caught in PollForCompletion. }
BEGIN
gPPCParamBlock.informParam.ioCompletion := NIL;
gPPCParamBlock.informParam.portRefNum := gPortRefNum;
gPPCParamBlock.informParam.autoAccept := TRUE;
gPPCParamBlock.informParam.portName := @gPPCPort;
gPPCParamBlock.informParam.locationName := @gLocationName;
gPPCParamBlock.informParam.userName := @gUserName;
err := PPCInform(PPCInformPBPtr(@gPPCParamBlock), TRUE);
END;
In this code, MyPPCInform calls the PPCInform function asynchronously with no
completion routine (ioCompletion is NIL). The program can then continue to do other
things while periodically calling the PollForCompletion procedure to find out when the
asynchronous call completes.
PROCEDURE PollForCompletion;
BEGIN
IF gPPCParamBlock.informParam.ioResult <= noErr THEN
BEGIN { The call has completed. }
IF gPPCParamBlock.informParam.ioResult = noErr THEN
BEGIN
{ The call completed successfully. }
END
ELSE
BEGIN
{ The call failed, handle the error. }
END;
END;
END;
PollForCompletion checks the value of the ioResult field to find out whether
PPCInform has completed. If the call has completed, PollForCompletion checks for an
arror condition and then performs an appropriate action.
There are three important things to note in this example of polling for completion:
• The parameter block passed to PPCInform, gPPCParamBlock, is a
program global variable. Since the parameter block passed to an asynchronous
call is owned by the system until the call completes, the parameter block must
not be declared as a local variable within the routine that makes the
asynchronous call. The memory used by local variables is released to the stack
when a routine ends, and if that part of the stack gets reused, the parameter
block, which could still be part of an operating system queue, can get trashed,
causing either unexpected results or a system crash. Always declare
parameter blocks globally or as nonrelocatable objects in the heap.
• Calls to PollForCompletion must be made from a program loop that's not
executed completely at interrupt time. This prevents deadlock. You don't
necessarily have to poll from an application's event loop (which is executed at
noninterrupt time), but if you poll from code that executes at interrupt time,
that code must give up control between polls.
• PollForCompletion checks the ioResult field of the parameter block to
determine whether PPCInform completed and, if it completed, to see if it
completed successfully.
One drawback to polling for completion is latency. When the asynchronous routine
completes its job, your program won't know it until the next time you poll. This can be
wasted time. For example, assume you give someone a task and ask them if they're done
(poll) only once a day: if they finish the task after an hour, you won't find out they've
completed the task until 23 hours later (a 23-hour latency). To avoid latency, use
completion routines instead of polling ioResult to find out when a routine completes.
USING COMPLETION ROUTINES
Making an asynchronous call with a completion routine is only slightly more complex
than polling for completion. A completion routine is a procedure that's called as soon as
the asynchronous function completes its task. When your program fills in the
parameter block to pass to the function, it sets the ioCompletion field to point to the
completion routine. Then, after calling the function asynchronously, your program can
continue. When the function completes, the system interrupts the program that's
running and the completion routine is executed. (There are some special things you
need to know about function results to use this model; see "Function Results and
Function Completion.")
Since the completion routine is executed as soon as the function's task is complete,
your program finds out about completion immediately and can start processing the
results of the function. Using a completion routine is like giving someone a task and
then asking them to call you as soon as they've completed it.
Because a completion routine may be called at interrupt time, it can't assume things
that most application code can. When a completion routine for an asynchronous
function gets control, the system is in the following state:
• On entry, register A0 points to the parameter block used to make the
asynchronous call.
• Your program again owns the parameter block used to make the
asynchronous call, which means you can reuse the parameter block to make
another asynchronous call (see the section "Call Chaining" later in this
article).
• Both register D0 and ioResult in the parameter block contain the result
status from the function call.
• For completion routines called by the File Manager or Device Manager,
the A5 world is undefined and must be restored before the completion routine
uses any application global variables.
Since completion routines execute at interrupt time, they must follow these rules:
• They must preserve all registers except A0, A1, and D0-D2.
• They can't call routines that can directly or indirectly move memory, and
they can't depend on the validity of handles to unlocked blocks.
• They shouldn't perform time-consuming tasks, because interrupts may
be disabled. As pointed out in the Macintosh Technical Note "NuBusTM
Interrupt Latency (I Was a Teenage DMA Junkie)," disabling interrupts and
taking over the machine for long periods of time "almost always results in a
sluggish user interface, something which is not usually well received by the
user." Some ways to defer time-consuming tasks are shown later in this
article.
• They can't make synchronous calls to device drivers, the File Manager, or
the PPC Toolbox for the reasons given earlier.
PPC Toolbox completion routines. The PPC Toolbox simplifies the job of writing
completion routines. When a PPC Toolbox function is called asynchronously, the
current value of register A5 is stored. When the completion routine for that call is
executed by the PPC Toolbox, the stored A5 value is restored and the parameter block
pointer used to make the call is passed as the input parameter to the completion
routine.
A completion routine called by the PPC Toolbox has this format in Pascal:
PROCEDURE MyCompletionRoutine (pbPtr: PPCParamBlockPtr);
PPC Toolbox completion routines are still called at interrupt time and so must follow
the rules of execution at interrupt time.
The following code shows an asynchronous PPCInform call and its completion routine.
PROCEDURE InformComplete (pbPtr: PPCParamBlockPtr);
BEGIN
IF pbPtr^.informParam.ioResult = noErr THEN
BEGIN
{ The PPCInform call completed successfully. }
END
ELSE
BEGIN
{ The PPCInform call failed; handle the error. }
END;
END;
PROCEDURE DoPPCInform;
VAR
err: OSErr;{ Error conditions are ignored in this procedure }
{ because they are caught in InformComplete. }
BEGIN
gPPCParamBlock.informParam.ioCompletion := @InformComplete;