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;
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, DoPPCInform calls PPCInform asynchronously with a completion routine
(ioCompletion contains a pointer to InformComplete). The program can then continue to do other
things.
When PPCInform completes, control is passed to InformComplete with a pointer to
gPPCParamBlock. InformComplete checks the result returned by PPCInform and then performs an
appropriate action.
Here are the important things to note in this example of a PPC Toolbox completion routine:
* The parameter block gPPCParamBlock is declared globally for the reasons given
earlier in the section "Polling for Completion."
* InformComplete checks the ioResult field of the parameter block to determine
whether PPCInform completed successfully.
File Manager and Device Manager completion routines in high-level languages. File Manager and Device
Manager completion routines written in a high-level language such as Pascal or C are more
complicated than PPC Toolbox completion routines. They must take additional steps to get the
value in register A0 and, if program global variables will be used, restore register A5 to the
application's A5 value. The reason for this is that File Manager and Device Manager completion
routines are called with the pointer to the call's parameter block in register A0 and with the A5 world
undefined.
In most high-level languages, registers A0, A1, and D0-D2 are considered scratch registers by the
compiler and aren't preserved across routine calls. For this reason, you should not depend on register
values as input parameters to routines written in a high-level language. Examples of completion
routines inInside Macintoshand in several Macintosh Technical Notes use short inline assembly
routines to retrieve the value of register A0, in the following manner:
FUNCTION GetPBPtr: ParmBlkPtr;
{ Return the pointer value in register A0. }
INLINE $2E88; { MOVE.L A0,(A7) }
PROCEDURE MyCompletionRoutine;
{ This procedure gets called when an asynchronous call completes. }
VAR
pbPtr: ParmBlkPtr;
BEGIN
pbPtr := GetPBPtr;{ Retrieve the value in register A0. }
DoWork(pbPtr); { Call another routine to do the actual work. }
END;
Although the GetPBPtr inline assembly routine works with today's compilers, be careful, because
register A0 could be used by the compiler for some other purpose before the statement with the
inline assembly code is executed. As shown in the previous example, you can minimize the chances of
the compiler using a register before you retrieve its value by retrieving the register value in the
completion routine's first statement and then doing as little as possible within the completion routine
(call another routine to do any additional work).
The safest way to use register values as input parameters to completion routines written in a high-
level language is to use a completion routine written in assembly language that calls a routine written
in a high-level language. The following record type allows File Manager and Device Manager
completion routines to be written in high-level languages such as C or Pascal with only one small
assembly language routine. This record also holds the application's A5 value so that the completion
routine can restore A5 and application globals can be accessed from within the completion routine.
TYPE
extendedPBPtr = ^extendedPB;
extendedPB = RECORD
ourA5: LONGINT; { Application's A5 }
ourCompletion: ProcPtr; { Address of the completion routine }
{ written in a high-level language }
pb: ParamBlockRec;{ Parameter block used to make call }
END;
PreCompletion, a small assembly language routine, is used as the completion routine for all File
Manager and Device Manager asynchronous calls (PreCompletion comes preassembled and ready to
link with your C or Pascal code on theDeveloper CD Seriesdisc). PreCompletion preserves the A5
register, sets A5 to the application's A5, calls the designated Pascal completion routine with a pointer
to the parameter block used to make the asynchronous call, and then restores the A5 register:
PreCompletion PROC EXPORT
LINK A6,#0 ; Link for the debugger.
MOVEM.L A5,-(SP) ; Preserve A5 register.
MOVE.L A0,-(SP) ; Pass PB pointer as the parameter.
MOVE.L -8(A0),A5 ; Set A5 to passed value (ourA5).
MOVE.L -4(A0),A0 ; A0 = real completion routine address.
JSR (A0) ; Transfer control to ourCompletion.
MOVEM.L (SP)+,A5 ; Restore A5 register.
UNLK A6 ; Unlink.
RTS ; Return.
STRING ASIS
DC.B $8D,'PreCompletion' ; The debugger string.
DC.W $0000
STRING PASCAL
ENDP
END
Before an application makes an asynchronous call, it initializes the extendedPB record with the
application's A5 and the address of the high-level language's completion routine. The ioCompletion
field of the extendedPB record's parameter block is initialized with the address of PreCompletion:
myExtPB.ourA5 := SetCurrentA5;
myExtPB.ourCompletion := @MyCompletionRoutine;
myExtPB.pb.ioCompletion := @PreCompletion;
The high-level language's completion routine called by PreCompletion has this format in Pascal:
PROCEDURE MyCompletionRoutine (pbPtr: ParmBlkPtr);
When MyCompletionRoutine is called, register A5 has been set to the stored application A5 and
pbPtr points to the parameter block (within the extended parameter block) used to make the
asynchronous call.
The rest of this article shows how to use asynchronous calls and completion routines to your
program's advantage and describes various techniques for working around the limitations imposed on
completion routines.
THE BIG THREE TECHNIQUES
There are lots of techniques you can use when working with asynchronous calls. Most are useful for
solving only one or two programming problems. This section describes the three most useful
techniques -- the use of operating system queues, call chaining, and extended parameter blocks.
OPERATING SYSTEM QUEUES
After reading the description of operating system queues inInside MacintoshVolume II, you might
assume they're for use only by the operating system. Wrong! Any program can create an OS queue
for its own purposes. OS queues are very useful in interrupt-time code such as completion routines,
because the two routines that manipulate OS queues, Enqueue and Dequeue, have the following
characteristics:
- They disable all interrupts while they update the queue. This is very important
because it prevents race conditions between interrupt and noninterrupt code
accessing the queue. (See "Race Conditions and OS Queues.")
- They can be called at interrupt time, because they don't move memory -- they
only manipulate a linked list of queue elements.
- They're very fast and efficient, so they won't be time-consuming operations in
your completion routines.
An OS queue owned by your program can hold queue elements defined by the system or queue
elements of your own design. A queue element is a record that starts with two fields, qLink and
qType. The qLink field is a QElemPtr that links queue elements together while they're in an OS
queue. The qType field is an integer value that identifies the queue element type. In OS queues
owned by your program, you may not need to use the qType field unless the OS queue can hold
more than one type of queue element. Here's how the system defines a queue element:
QElem = RECORD
qLink: QElemPtr; { Link to next queue element. }
qType: INTEGER; { Queue element type. }
{ Add your data fields here. }
END;
The following record types are some of the system-defined queue elements: ParamBlockRec,
CInfoPBRec, DTPBRec, HParamBlockRec, FCBPBRec, WDPBRec, CMovePBRec,
MPPParamBlock, ATPParamBlock, XPPParamBlock, DSPParamBlock, EParamBlock,
PPCParamBlockRec, TMTask, DeferredTask, and VBLTask.
To use an OS queue in your program, you need to allocate a queue header (QHdr) variable and
possibly define your own queue element type:
TYPE
{ Define a queue element type. }
MyQElemRecPtr = ^MyQElemRec;
MyQElemRec = RECORD
qLink: QElemPtr;
qType: INTEGER;
myData: myDataType; { Put any data fields you want here. }
END;
VAR
{ Allocate a queue element and a queue header. }
myQElem: MyQElemRec;
myOSQueueHdr: QHdr;
You must initialize the queue header before it's used by setting its qHead and qTail fields to NIL:
{ Initialize the OS queue. }
myOSQueueHdr.qHead := NIL;
myOSQueueHdr.qTail := NIL;
The queue element can then be added to the OS queue:
{ Add myQElem to the queue. }
Enqueue(QElemPtr(@myQElem), @myOSQueueHdr);
This code shows how to remove a queue element (in this example, the first item in the queue) from
an OS queue before using it:
VAR
myQElemPtr: MyQElemRecPtr;
myQElemPtr := MyQElemRecPtr(myOSQueueHdr.qHead);
IF myQElemPtr <> NIL THEN { Make sure we have a queue element. }
BEGIN
IF Dequeue(QElemPtr(myQElemPtr), @myOSQueueHdr) =
noErr THEN
BEGIN
{ We successfully removed the queue element from }
{ the queue, so we can use myQElemPtr^.myData. }
{ In this example, we'll put the queue element back }
{ in the queue when we're done with it. }
Enqueue(QElemPtr(myQElemPtr), @myOSQueueHdr);
END
ELSE
BEGIN
{ Someone else just claimed the queue element }
{ between the two IF statements and we just avoided }
{ a race condition! Try again later. }
END;
END;
OS queues owned by your program can be used for many purposes, including these:
- Completion routines can schedule work to be done by your application's event
loop by putting requests into an OS queue.
- Extra buffers or parameter blocks needed by completion routines can be put into
an OS queue by code called from the program's event loop. These buffers or
parameter blocks can be safely claimed and used by code running at interrupt
time.
- Completion routines can schedule the processing of a completed call by putting
the parameter block used to make the call into an OS queue. This is useful when
the processing might move memory or take too much time and so can't be
performed in the completion routine.
- Data accessed and manipulated by both interrupt code and noninterrupt code can
be protected from race conditions if it's stored in a queue element and the
Dequeue and Enqueue routines are used to claim and release ownership of the
data from an OS queue (as described earlier in "Race Conditions and OS
Queues").
CALL CHAINING
When a multistep operation is performed via multiple asynchronous calls with completion routines,
it's calledcall chaining. Each asynchronous call's completion routine reuses the parameter block
passed to the completion routine to make the next asynchronous call. Call chaining from completion
routines allows your program to start the next step in a multistep operation with no latency (see
Figure 2).
To use call chaining, you must design your call chain; that is, you must decide the order of the
asynchronous calls you want to make. For each completion routine, determine what step should be
taken if the previous call completed successfully with no error condition and what step should be
taken if the previous call completed with an error.
A chained call sequence may have several end points or breaks in the chain, depending on what
you're trying to accomplish and what conditions are encountered along the way. For example, you
may not want to make another asynchronous call because an error condition occurred, because the next step your program needs to take involves a call that can't be made at interrupt time, or because
all steps were completed successfully. The easiest way for your chained call sequence to pass control
back to noninterrupt code is through an OS queue. This technique is shown in the section "Putting
the Big Three Together."
EXTENDING PARAMETER BLOCKS
Unless you do a little extra work, a completion routine is somewhat isolated from the rest of your
program. The only data accessible to a completion routine when it executes is the parameter block
used to make the asynchronous call and, if you preserve and restore A5, the program's global
variables. As noted before, you must be careful to avoid race conditions when accessing global
variables.
Figure 2 Call Chaining
You can extend a parameter block by attaching your own data to the end of a parameter block, like
this:
TYPE
myPBRecPtr = ^myPBRec;
myPBRec = RECORD
pb: ParamBlockRec;
myData: myDataType; { Put any data type you want here. }
END;
From within a completion routine, using the extended fields is easy:
IF thePBRecPtr^.pb.ioResult = noErr THEN
thePBRecPtr^.myData := kSomeValue;
Extending a parameter block has several benefits for asynchronous program code:
- By extending a parameter block to include all variables used by the routine, you
can reduce the amount of stack space used by completion routines.
- By keeping all data associated with a particular session in the extended parameter
block, you can support multiple independent sessions.
- By putting values needed by a completion routine in an extended parameter block
instead of in program global variables, you can prevent race conditions. This
provides noninterrupt code and interrupt code with a safe method to
communicate.
PUTTING THE BIG THREE TOGETHER
Now that you know about OS queues, call chaining, and extending parameter blocks, let's look at a
simple example of how these techniques can be used together. PPC Toolbox calls, being slightly
simpler, are used in this example.
In the example, the program is to receive and accept a PPC session request, read some data, process
the data, and then close the connection. To accomplish this, the program calls PPCInform
asynchronously with a completion routine, has PPCInform's completion routine call PPCRead
asynchronously with a completion routine, and then has PPCRead's completion routine schedule
processing of the data by putting a request into an OS queue. After the data is removed from the
queue and processed in the application's main event loop, the program calls PPCClose
asynchronously with a completion routine and has PPCClose's completion routine call PPCInform
again to wait for another connection.
We begin with an extended PPC parameter block record that can hold all the data the program
needs to access from the various procedures:
CONST
kPPCIOBufSize = 1024; { Size of the I/O buffer. }
TYPE
PPCIOBuffer = ARRAY[1..kPPCIOBufSize] OF SignedByte;
PPCSessRecPtr = ^PPCSessRec;
PPCSessRec = RECORD
pb: PPCParamBlockRec;{ The pb must be first. }
err: OSErr; { To catch results. }
sessPortName: PPCPortRec; { Port name returned to }
{ PPCInform. }
sessLocationName: LocationNameRec; { Location name returned }
{ to PPCInform. }
sessUserName: Str32; { User name returned to }
{ PPCInform. }
buffer: PPCIOBuffer; { R/W buffer used by this }
{ session. }
END;
Next, we declare the global variables used in this example:
VAR
gQuitting: BOOLEAN; { True when no new sessions should }
{ be allowed. }
gPortRefNum: PPCPortRefNum; { PPC port reference number from }
{ PPCOpen. }
gReadQueue: QHdr; { Where PPCRead parameter blocks }
{ are scheduled to be processed. }
gDoneQueue: QHdr; { Where parameter blocks are put }
{ when completion routines are done }
{ with them. }
Several procedures are used in the example: DoPPCInform, InformComplete, ReadComplete,
ProcessPPCData, EndComplete, and HandlePPCErrors. Not shown in this article is the program
code for such operations as opening the PPC port, setting gQuitting to FALSE, and initializing the
two OS queue headers before DoPPCInform is called. DoPPCInform simply fills in the parameter block, previously allocated by the program and passed to
DoPPCInform, and calls PPCInform asynchronously with InformComplete as the completion
routine. Any errors returned by PPCInform will be handled by InformComplete.
PROCEDURE DoPPCInform (pbPtr: PPCSessRecPtr);
BEGIN
{ Call PPCInform. }
PPCInformPBPtr(pbPtr)^.ioCompletion := @InformComplete;
PPCInformPBPtr(pbPtr)^.portRefNum := gPortRefNum;
PPCInformPBPtr(pbPtr)^.autoAccept := TRUE;
PPCInformPBPtr(pbPtr)^.portName := @pbPtr^.sessPortName;
PPCInformPBPtr(pbPtr)^.locationName := @pbPtr^.sessLocationName;
PPCInformPBPtr(pbPtr)^.userName := @pbPtr^.sessUserName;
{ Error conditions are ignored in this procedure because they }
{ are caught in InformComplete. }
pbPtr^.err := PPCInformAsync(PPCInformPBPtr(pbPtr));
{ Continued at InformComplete. }
END;
InformComplete is called when PPCInform completes. InformComplete first checks for errors from
PPCInform. If the result is noErr, InformComplete fills in the parameter block and calls PPCRead
asynchronously with ReadComplete as the completion routine. Any errors returned by PPCRead will
be handled by ReadComplete. If PPCInform failed (the result is not noErr), InformComplete puts
the parameter block into gDoneQueue, where the error condition can be handled from the
program's event loop.
PROCEDURE InformComplete (pbPtr: PPCSessRecPtr);
BEGIN
IF PPCInformPBPtr(pbPtr)^.ioResult = noErr THEN
{ The PPCInform call completed successfully. }
BEGIN
{ Call PPCRead. }
PPCReadPBPtr(pbPtr)^.ioCompletion := @ReadComplete;
{ We're reusing the same parameter block, so the }
{ sessRefNum is already filled in. }
PPCReadPBPtr(pbPtr)^.bufferLength := kPPCIOBufSize;
PPCReadPBPtr(pbPtr)^.bufferPtr := @pbPtr^.buffer;
{ Error conditions are ignored in this procedure }
{ because theyare caught in ReadComplete. }
PPCSessRecPtr(pbPtr)^.err :=
PPCReadAsync(PPCReadPBPtr(pbPtr));
{ Continued at ReadComplete. }
END
ELSE
{ The PPCInform call failed. Drop the parameter block in }
{ the "done" queue for handling later. }
Enqueue(QElemPtr(pbPtr), @gDoneQueue);
{ Dequeued by HandlePPCErrors. }
END;
ReadComplete is called when PPCRead completes. ReadComplete first checks for errors from
PPCRead. If the result is noErr, ReadComplete puts the parameter block into gReadQueue. If
PPCRead failed (the result is not noErr), ReadComplete puts the parameter block into gDoneQueue.
In either case, the information queued is handled from the program's event loop.
PROCEDURE ReadComplete (pbPtr: PPCParamBlockPtr);
BEGIN
IF PPCReadPBPtr(pbPtr)^.ioResult = noErr THEN
{ The PPCRead call completed successfully. Drop the }
{ parameter block in the "read" queue for }
{ handling later. }
Enqueue(QElemPtr(pbPtr), @gReadQueue)
{ Dequeued by ProcessPPCData. }
ELSE
{ The PPCRead call failed. Drop the parameter block }
{ in the "done" queue for handling later. }
Enqueue(QElemPtr(pbPtr), @gDoneQueue)
{ Dequeued by HandlePPCErrors. }
END;
ProcessPPCData is called regularly from the program's event loop. If gReadQueue contains a
parameter block, ProcessPPCData removes the parameter block from the queue and processes the
data read in the PPCSessRec's buffer. After processing the data, ProcessPPCData calls PPCEnd
asynchronously with EndComplete as the completion routine. Any errors returned by PPCEnd will
be handled by EndComplete.
PROCEDURE ProcessPPCData;
VAR
pbPtr: PPCSessRecPtr;
BEGIN
{ Check for a parameter block in the queue. }
IF gReadQueue.qHead <> NIL THEN
BEGIN
{ Get the PPCSessRec and remove it from the queue. }
pbPtr := PPCSessRecPtr(gReadQueue.qHead);
IF Dequeue(QElemPtr(pbPtr), @gReadQueue) = noErr THEN
BEGIN
{ Process PPCReadPBPtr(pbPtr)^.actualLength }
{ bytes of data in the data buffer, }
{ pbPtr^.buffer, here.Then call PPCEnd to end }
{ the session. }
PPCEndPBPtr(pbPtr)^.ioCompletion := @EndComplete;
{ Error conditions are ignored in this }
{ procedure because they are caught in }
{ EndComplete. }
pbPtr^.err := PPCEndAsync(PPCEndPBPtr(pbPtr));
{ Continued at EndComplete. }
END;
END;
END;
EndComplete is called when PPCEnd completes. It first checks for errors from PPCEnd. If the
result is noErr, EndComplete either calls DoPPCInform to call PPCInform asynchronously again or
puts the parameter block into gDoneQueue. If PPCEnd failed (the result is not noErr),
EndComplete puts the parameter block into gDoneQueue. Any queued information is handled from
the program's event loop.
PROCEDURE EndComplete (pbPtr: PPCParamBlockPtr);
BEGIN
IF PPCEndPBPtr(pbPtr)^.ioResult = noErr THEN
BEGIN { The PPCEnd call completed successfully. }
IF NOT gQuitting THEN
{ Reuse the parameter block for another PPCInform. }
DoPPCInform(PPCSessRecPtr(pbPtr))
{ Continued at DoPPCInform and then InformComplete. }
ELSE
{ Drop the parameter block in the "done" }
{ queue for handling later. }
Enqueue(QElemPtr(pbPtr), @gDoneQueue);
{ Dequeued by HandlePPCErrors. }
END
ELSE
BEGIN { The PPCEnd call failed. }
{ Drop the parameter block in the "done" queue }
{ for handling later. }
Enqueue(QElemPtr(pbPtr), @gDoneQueue);
{ Dequeued by HandlePPCErrors. }
END;
END;
HandlePPCErrors is called regularly from the program's event loop. If gDoneQueue contains any
parameter blocks, HandlePPCErrors removes the parameter blocks from the queue one at a time,
checks to see what PPC call failed by inspecting the csCode field of the parameter block, and then
handles the error condition appropriately. If the call that failed was PPCRead or PPCWrite,
HandlePPCErrors calls PPCEnd asynchronously with EndComplete as the completion routine. Any
errors returned by PPCEnd will be handled by EndComplete.
PROCEDURE HandlePPCErrors;
CONST
{ PPC csCodes from async calls. }
ppcOpenCmd = 1;
ppcStartCmd = 2;
ppcInformCmd = 3;
ppcAcceptCmd = 4;
ppcRejectCmd = 5;
ppcWriteCmd = 6;
ppcReadCmd = 7;
ppcEndCmd = 8;
ppcCloseCmd = 9;
IPCListPortsCmd = 10;
VAR
pbPtr: PPCSessRecPtr;
BEGIN
{ Process any parameter blocks in the queue. }
WHILE gDoneQueue.qHead <> NIL DO
BEGIN
{ Get the PPCSessRec and remove it from the queue. }
pbPtr := PPCSessRecPtr(gDoneQueue.qHead);
IF Dequeue(QElemPtr(pbPtr), @gDoneQueue) = noErr THEN
CASE PPCEndPBPtr(pbPtr)^.csCode OF
ppcOpenCmd..ppcRejectCmd,
ppcEndCmd..IPCListPortsCmd:
{ For these calls, we'll just dispose of }
{ the parameter block. }
DisposePtr(Ptr(pbPtr));
ppcWriteCmd, ppcReadCmd:
BEGIN
{ We need to call PPCEnd after read or }
{ write failures to clean up after this }
{ session. }
PPCEndPBPtr(pbPtr)^.ioCompletion :=
@EndComplete;
{ Error conditions are ignored in this }
{ procedure because they are caught in }
{ EndComplete. }
pbPtr^.err :=
PPCEndAsync(PPCEndPBPtr(pbPtr));
{ Continued at EndComplete. }
END;
END;
END;
END;
In this example of extending parameter blocks and using OS queues and call chaining, notice that
asynchronous calls are chained together until an operation that can't be accomplished at interrupt
time is necessary; then the extended parameter block is put into an OS queue where the mainprogram can access it. Very few global variables are needed because OS queues are used to hold any
data the main program code needs to access. Local variables aren't needed by the completion
routines because the extended parameter block, PPCSessRec, holds everything the completion
routines need.
DEBUGGING HINTS
Here are the top five debugging hints for writing asynchronous code.
Use Debugger or DebugStr calls and a low-level debugger. Because completion routines are called by the
system, usually as a result of an interrupt, source-level debuggers don't work with completion
routines. If you're having problems with a completion routine, first look at the parameter block used
to make the asynchronous call. Look both before and after the call by using a Debugger or DebugStr
call just before you make the asynchronous call and again at the beginning of the completion routine
(remember, register A0 points to the parameter block).
Make sure parameter blocks are around for the life of the asynchronous call. The parameter block will
have a whole new meaning if you forget and allocate a parameter block on the local stack, then make
an asynchronous call with it, leave the current procedure or function, and reuse the stack for
something new. There's nothing the system hates more than a completely bogus parameter block. If
you check your parameter block at completion time and the contents are different from what you
expected, you've probably done this.
Don't reuse a parameter block that's in use. A parameter block passed to an asynchronous call is owned
by the operating system until the asynchronous call completes. If you reuse the parameter block
before the asynchronous call completes, at least one of the calls made with the parameter block will
fail or crash the system. This can happen if you use a parameter block once from one place in your
program and then forget and use it again from somewhere else.
Global parameter blocks should be avoided, because they're easy to reuse from several places within a
program. If you keep your unused parameter blocks in an OS queue, you can safely claim one and
reuse it anytime.
Avoid SyncWait. Does your Macintosh just sit there not responding to user events? Drop into the
debugger and take a look at the code that's executing. Does it look like this?
MOVE.W $0010(A0),D0
BGT.S -$04,(PC)
That's SyncWait, the routine that synchronous calls sit in while waiting for a request to complete.
Register A0 points to the parameter block used to make the call, offset $10 is the ioResult field of the
parameter block, and SyncWait is waiting for ioResult to be less than or equal to 0 (noErr).
The ioResult field is changed by code executing as a result of an interrupt. If interrupts are disabled
(because the synchronous call was made at interrupt time) or if the synchronous call was made to a
service that's busy, you'll be in SyncWait forever. Take a look at the parameter block and where it is
in memory, and you'll probably be able to figure out which synchronous call was made at interrupt
time and which program made it.
Leave a trail of bread crumbs. There's nothing harder than reading chained asynchronous source code
with no comments. You should always use comments to remind yourself where your chained call
sequence goes. In the PPC code example given above, I left comments like "Continued at
EndComplete" or "Dequeued by ProcessPPCData" to remind me where the chained call sequence
will resume execution.
COMMON PROBLEMS AND THEIR SOLUTIONS
This section warns of some common problems and suggests ways to work around them.
TIME-CONSUMING TASKS AT INTERRUPT TIME
You may find a situation where a completion routine needs to perform some time-consuming task,
but that task can be performed from interrupt-time code. This is a situation where the Deferred Task
Manager may be useful. The Deferred Task Manager allows you to improve interrupt handling by
deferring a lengthy task until all interrupts can be reenabled, but while still within the hardware
interrupt cycle.
WAITNEXTEVENT SLEEP LATENCY
If you set your sleep time to a large value, maybe because you've been switched out, polling from the
program's event loop may cause additional latency. The Process Manager's WakeUpProcess call,
when available, can be used to shorten the time between when a completion routine queues a
parameter block and when your program's event loop polls the queue header and processes the data
in the parameter block. WakeUpProcess does this by making your program eligible for execution
before the sleep time passed to WaitNextEvent expires.
The only parameter passed to WakeUpProcess is the process serial number of the process you want
to wake up. You'll need to get your program's process serial number with the GetCurrentProcess
function and add it to the extended parameter block used to call asynchronous functions:
{ Zero the process serial number. }
myPB.myPSN.highLongOfPSN := 0;
myPB.myPSN.lowLongOfPSN := 0;
{ Make sure the Process Manager is available. }
IF Gestalt(gestaltOSAttr, myFeature) = noErr THEN
IF GetCurrentProcess(myPB.myPSN) = noErr THEN
; { Either we got the process serial number or it's still }
{ zero. }
The completion routine would use the process serial number (if available) to wake up your program
immediately after queueing a parameter block:
{ Drop the parameter block in the "done" queue for handling }
{ later. }
Enqueue(QElemPtr(pbPtr), @gDoneQueue);
{ If we have a process serial number (myPSN <> 0), wake up the }
{ process. }
IF (pbPtr^.myPSN.highLongOfPSN<>0) OR
(pbPtr^.myPSN.lowLongOfPSN<>0) THEN
IF WakeUpProcess(pbPtr^.myPSN) = noErr THEN
; { Wake up the process. }
STACK SPACE AND CODE EXECUTING AT INTERRUPT LEVEL
Have you ever thought about the stack space used by interrupt code? Where does it come from?
How much is available? Good questions.
When interrupt code (including completion routines) is called, it borrows space from whatever stack
happens to be in use at the time. That means you have no control over the amount of stack space
available, and so should use as little of the stack as possible.
At times, very little stack space is available, because some common Macintosh system calls
temporarily use large portions of the stack. For example, some QuickDraw routines may leave as
little as 1600 bytes of stack space and MoveHHi can leave as little as 1024 bytes of stack space under
certain conditions. That's not a lot of space to borrow. If your interrupt code will call a routine that
uses more than a few bytes of stack space, you should call the StackSpace function before calling that
routine.
The typical symptom of using too much stack space is a random crash, because the memory you trash
by overflowing the stack could belong to any program -- including the Macintosh Operating System.
Here are the easiest ways to reduce the amount of stack space used by interrupt code:
- Don't use local variables. Either extend your parameter block to hold any variables
needed by your completion routine or keep an OS queue of buffers that can be
used by your completion routine.
- Try to keep the number of calls from your completion routine to other routines to
a minimum. Each routine you call uses part of the stack to build a stack frame.
PROBLEMS WITH REUSING PARAMETER BLOCKS
There are three problems you may run into when you reuse a parameter block: unfortunate
coercions, unfortunate overlap, and garbage in the parameter block.
Unfortunate coercions. Make sure parameter blocks are large enough for every use you'll put them to.
For example, if you use a parameter block for both PPC Toolbox calls and File Manager calls, make
sure the parameter block is large enough to hold any of the parameter blocks used by either manager.
One way to do this is with a variant record:
variantPBRec = RECORD
CASE INTEGER OF
1: (PB: ParamBlockRec); { File Manager parameter blocks }
2: (cInfoPB: CInfoPBRec);
3: (dtPB: DTPBRec);
4: (hPB: HParamBlockRec);
5: (cMovePB: CMovePBRec);
6: (wdPB: WDPBRec);
7: (fcbPB: FCBPBRec);
8: (ppcPB: PPCParamBlockRec); { PPC Toolbox parameter block }
END;
Unfortunate overlap. Don't assume variant record fields with the same name are in exactly the same
place in the variant record. If they aren't, you'll run into problems with overlap. Check first and be
sure.
Garbage in the parameter block. When reusing a parameter block, make sure data from the last use
doesn't affect the next call. Always initialize all input fields. Many programmers go one step further
by clearing the entire parameter block to zeros before initializing the input fields.
COMPLETION ORDER MIXUPS
Don't depend on a service being single-threaded (requests executed one at a time) or on requests
being handled in the order they were made (first in, first out). The File Manager is single-threaded,
but requests may not always be handled in the order they were made. The AppleTalk drivers allow
multiple requests to execute concurrently.
If the order of completion is important, don't use concurrent calls -- use chained calls. For example,
if you write some data and then expect to read that data back, don't start an asynchronous write and
then start an asynchronous read before the write completes. If the calls aren't handled in the order
they were made, the read may complete before the write.
POLLING PROBLEMS
If your application that polls for completion works great when it's the current application, but slows
down dramatically or stops when it's in the background, check for these common problems.
The canBackground bit. If you forget to set the canBackground bit in the SIZE resource, your
application's event loop won't get called with null events while your application is in the background.
If you're depending on null events for polling, your program won't poll while it's in the background.
Large WaitNextEvent sleep values. Did you crank up the sleep value passed to WaitNextEvent when
your application received a suspend event? Talk about additional latency! This will do it if you're
polling from the event loop.
What are other applications doing? Other applications can slow your event handling down by not
calling WaitNextEvent regularly. If your polling from the event loop slows down because of that,
there's not a lot you can do about it.
If your polling stops when another application is in the foreground, it could be that the other
application isn't handling its update events. See Macintosh Technical Note "Pending Update Perils"
for a description of this problem.
Polling from VBL tasks in an application's heap. VBL tasks in your application's heap are removed from
the VBL queue during a context switch when your application is switched out and are added to the
VBL queue when your applicaton is switched back in. VBL tasks in the system heap are unaffected by
context switches.
If you poll from a VBL task and don't want polling to stop when your application is switched out,
make sure you put your VBL task in the system heap.
COMPLETION
There are many situations where synchronous calls work well. However, there are times when
asynchronous calls must be used to prevent system deadlock or to let your program continue
execution while waiting for time-consuming calls to complete. Understanding the material covered in
this article should help you understand when to use asynchronous calls and give you the techniques
needed to avoid the problems commonly encountered in code that executes asynchronously.
RECOMMENDED READING
- Inside Macintosh Volume II (Addison-Wesley, 1985), Chapter 6, "The Device Manager," Chapter 10,
"The AppleTalk Manager," and Chapter 13, "The Operating System Utilities."
- Inside Macintosh Volume V (Addison-Wesley, 1986), Chapter 28, "The AppleTalk Manager."
- Inside Macintosh Volume VI (Addison-Wesley, 1991), Chapter 7, "The PPC Toolbox," Chapter 29, "The
Process Manager," and Chapter 32, "The AppleTalk Manager."
- Inside Macintosh: Files (Addison-Wesley, 1992), Chapter 2, "The File Manager." Previously, Inside
Macintosh Volume II, Chapter 4.
- Inside Macintosh: Processes (Addison-Wesley, 1992), Chapter 6, "The Deferred Task Manager."
Previously, Inside Macintosh Volume V, Chapter 25 and Macintosh Technical Note #320, "Deferred Task
Traps, Truths, and Tips."
- Macintosh Technical Notes "MultiFinder Miscellanea" (formerly #180), "Setting and Restoring A5"
(formerly #208), "NuBus Interrupt Latency (I Was a Teenage DMA Junkie)" (formerly #221), and
"Pending Update Perils" (formerly #304).
DEADLOCK BY GORDON SHERIDAN
Deadlock is a state in which each of two or more processes is waiting for one of the other processes to
release some resource necessary for its completion. The resource may be a file, a global variable, or even
the CPU. The process could, for example, be an application's main event loop or a Time Manager task.
When deadlock occurs on the Macintosh, usually at least one of the processes is executing as the result of
an interrupt. VBL tasks, Time Manager tasks, Deferred Task Manager tasks, completion routines, and interrupt
handlers can all interrupt an application's main thread of execution. When the interrupted process is using a
resource that the interrupting process needs, the processes are deadlocked.
For example, suppose a Time Manager task periodically writes data to a file by making a synchronous
Write request, and an application reads the data from its main event loop. Depending on the frequency of
the task and the activity level of the File Manager, the Time Manager task may often write successfully.
Inevitably, however, the Time Manager task will interrupt the application's Read request and deadlock will
occur.
Because the File Manager processes only one request at a time, any subsequent requests must wait for the
current request to complete. In this case, the synchronous request made by the Time Manager task must wait
for the application's Read request to complete before its Write request will be processed. Unfortunately, the
File Manager must wait for the Time Manager task to complete before it can resume execution. Each process
is now waiting for the other to complete, and they'll continue to wait forever.
Synchronous requests at interrupt time tend to produce deadlock, because the call is queued for processing
and then the CPU sits and spins, waiting for an interrupt to occur, which signals that the request has been
completed. If interrupts are turned off, or if a previous pending request can't finish because it's waiting to
resume execution after the interrupt, the CPU will wait patiently (and eternally) for the request to finish --
until you yank the power cord from the wall.
RACE CONDITIONS AND OS QUEUES
When two or more processes share the same data, you must be careful to avoid
race conditions. A race
condition exists when data is simultaneously accessed by two processes. On the Macintosh, the two
processes are typically program code running with interrupts enabled and code executing at interrupt time
(such as a completion routine).
To prevent race conditions, you must have a method of determining ownership of the shared data. A shared
global flag isn't safe because there can be a race condition with the flag. For example, the following code
can't be used to claim ownership of a record:
{ This can cause a race condition. }
IF gRecFree THEN { Is record in use? }
BEGIN { It wasn't when we checked. }
gRecFree := FALSE; { Claim record. }
{ Use record. }
gRecFree := TRUE; { Release record. }
END;
A race condition can occur in this code because there's a small window of time between when the IF
statement's expression is evaluated and when the record is claimed. During this time the program can be
interrupted. The only way to prevent race conditions is to make the process of checking for and claiming
ownership a step that can't be interrupted.
OS queues and the Operating System Utility routines Enqueue and Dequeue provide a safe way to claim
ownership of data. Enqueue and Dequeue briefly disable interrupts while manipulating the queue, so they're
safe from race conditions.
To use an OS queue to protect data from race conditions, make the data part of a queue element and put
the queue element into an OS queue. Whenever any part of the program wants to manipulate data in the
queue element, it attempts to remove the queue element from the OS queue. If the queue element isn't in the
queue and so can't be removed, that means another process currently has ownership of the queue element
and the data within it.
FUNCTION RESULTS AND FUNCTION COMPLETION
BY SCOTT BOYD AND JIM LUTHER
Not all function results are equal. Ignore some, pay attention to others. Ignore function results from
asynchronous File Manager and PPC Toolbox calls. They contain no useful information. To get useful result
information, wait for the call to complete, then check ioResult or register D0; both contain the result.Both the File Manager and the PPC Toolbox will always call your completion routine if you specified one. If
you didn't supply one, and instead are polling, test ioResult in your parameter block. The call has completed
if ioResult is less than or equal to noErr.
Don't ignore function results from asynchronous Device Manager calls (for example, AppleTalk driver, Serial
Driver, and disk driver calls). The function result tells you whether the Device Manager successfully delivered
your request to the device driver. Success is indicated by noErr; any other value indicates failure.
The system calls your completion routine only if the Device Manager successfully delivered your request to
the driver. On completion, check whether your call succeeded by looking in ioResult or register D0.
COMPLETION ROUTINE ADDRESS GENERATIONWhen you fill a parameter block's ioCompletion field with the address of
a completion routine, your compiler has to calculate the address of the completion routine. Most compilers generate that
address either as a PC-relative reference (the address of the routine's entry point within the local code segment) or as an
A5-relative reference (the address of the routine's jump table entry). If your compiler generates an A5-relative reference,
the code that generates the address of the completion routine must run with the program's A5 world set up.
JIM LUTHER works in Apple Developer Technical Support, focusing on AppleTalk, the
File Manager, and other lower regions of the operating system. Jim uses a heap/stack-based organizational model in his
office: there are heaps or stacks of books, papers, disks, and hardware on every square inch of shelf space and over most
of the floor. He was last seen muttering to himself "Now where did I put that . . .?"*
MPW Pascal defaults to using PC-relative references when routines are in the same segment and uses A5-relative
references when routines are in a different segment. MPW C defaults to using A5-relative references. THINK Pascal and
THINK C always use A5-relative references. MPW Pascal and MPW C allow you to change their default method with the -
b compiler option.*
The Deferred Task Manager is fully described in Chapter 6, "The Deferred Task Manager," in Inside Macintosh: Processes. *
THANKS TO OUR TECHNICAL REVIEWERS Scott Boyd, Neil Day, Martin Minow, Gordon Sheridan *