TweetFollow Us on Twitter

March 93 - ASYNCHRONOUS ROUTINES ON THE MACINTOSH

ASYNCHRONOUS ROUTINES ON THE MACINTOSH

JIM LUTHER

[IMAGE Luther_rev1.GIF]

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.

[IMAGE Luther_rev2.GIF]

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.

[IMAGE Luther_rev3.GIF]

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 *

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Tokkun Studio unveils alpha trailer for...
We are back on the MMORPG news train, and this time it comes from the sort of international developers Tokkun Studio. They are based in France and Japan, so it counts. Anyway, semantics aside, they have released an alpha trailer for the upcoming... | Read more »
Win a host of exclusive in-game Honor of...
To celebrate its latest Jujutsu Kaisen crossover event, Honor of Kings is offering a bounty of login and achievement rewards kicking off the holiday season early. [Read more] | Read more »
Miraibo GO comes out swinging hard as it...
Having just launched what feels like yesterday, Dreamcube Studio is wasting no time adding events to their open-world survival Miraibo GO. Abyssal Souls arrives relatively in time for the spooky season and brings with it horrifying new partners to... | Read more »
Ditch the heavy binders and high price t...
As fun as the real-world equivalent and the very old Game Boy version are, the Pokemon Trading Card games have historically been received poorly on mobile. It is a very strange and confusing trend, but one that The Pokemon Company is determined to... | Read more »
Peace amongst mobile gamers is now shatt...
Some of the crazy folk tales from gaming have undoubtedly come from the EVE universe. Stories of spying, betrayal, and epic battles have entered history, and now the franchise expands as CCP Games launches EVE Galaxy Conquest, a free-to-play 4x... | Read more »
Lord of Nazarick, the turn-based RPG bas...
Crunchyroll and A PLUS JAPAN have just confirmed that Lord of Nazarick, their turn-based RPG based on the popular OVERLORD anime, is now available for iOS and Android. Starting today at 2PM CET, fans can download the game from Google Play and the... | Read more »
Digital Extremes' recent Devstream...
If you are anything like me you are impatiently waiting for Warframe: 1999 whilst simultaneously cursing the fact Excalibur Prime is permanently Vault locked. To keep us fed during our wait, Digital Extremes hosted a Double Devstream to dish out a... | Read more »
The Frozen Canvas adds a splash of colou...
It is time to grab your gloves and layer up, as Torchlight: Infinite is diving into the frozen tundra in its sixth season. The Frozen Canvas is a colourful new update that brings a stylish flair to the Netherrealm and puts creativity in the... | Read more »
Back When AOL WAS the Internet – The Tou...
In Episode 606 of The TouchArcade Show we kick things off talking about my plans for this weekend, which has resulted in this week’s show being a bit shorter than normal. We also go over some more updates on our Patreon situation, which has been... | Read more »
Creative Assembly's latest mobile p...
The Total War series has been slowly trickling onto mobile, which is a fantastic thing because most, if not all, of them are incredibly great fun. Creative Assembly's latest to get the Feral Interactive treatment into portable form is Total War:... | Read more »

Price Scanner via MacPrices.net

Early Black Friday Deal: Apple’s newly upgrad...
Amazon has Apple 13″ MacBook Airs with M2 CPUs and 16GB of RAM on early Black Friday sale for $200 off MSRP, only $799. Their prices are the lowest currently available for these newly upgraded 13″ M2... Read more
13-inch 8GB M2 MacBook Airs for $749, $250 of...
Best Buy has Apple 13″ MacBook Airs with M2 CPUs and 8GB of RAM in stock and on sale on their online store for $250 off MSRP. Prices start at $749. Their prices are the lowest currently available for... Read more
Amazon is offering an early Black Friday $100...
Amazon is offering early Black Friday discounts on Apple’s new 2024 WiFi iPad minis ranging up to $100 off MSRP, each with free shipping. These are the lowest prices available for new minis anywhere... Read more
Price Drop! Clearance 14-inch M3 MacBook Pros...
Best Buy is offering a $500 discount on clearance 14″ M3 MacBook Pros on their online store this week with prices available starting at only $1099. Prices valid for online orders only, in-store... Read more
Apple AirPods Pro with USB-C on early Black F...
A couple of Apple retailers are offering $70 (28%) discounts on Apple’s AirPods Pro with USB-C (and hearing aid capabilities) this weekend. These are early AirPods Black Friday discounts if you’re... Read more
Price drop! 13-inch M3 MacBook Airs now avail...
With yesterday’s across-the-board MacBook Air upgrade to 16GB of RAM standard, Apple has dropped prices on clearance 13″ 8GB M3 MacBook Airs, Certified Refurbished, to a new low starting at only $829... Read more
Price drop! Apple 15-inch M3 MacBook Airs now...
With yesterday’s release of 15-inch M3 MacBook Airs with 16GB of RAM standard, Apple has dropped prices on clearance Certified Refurbished 15″ 8GB M3 MacBook Airs to a new low starting at only $999.... Read more
Apple has clearance 15-inch M2 MacBook Airs a...
Apple has clearance, Certified Refurbished, 15″ M2 MacBook Airs now available starting at $929 and ranging up to $410 off original MSRP. These are the cheapest 15″ MacBook Airs for sale today at... Read more
Apple drops prices on 13-inch M2 MacBook Airs...
Apple has dropped prices on 13″ M2 MacBook Airs to a new low of only $749 in their Certified Refurbished store. These are the cheapest M2-powered MacBooks for sale at Apple. Apple’s one-year warranty... Read more
Clearance 13-inch M1 MacBook Airs available a...
Apple has clearance 13″ M1 MacBook Airs, Certified Refurbished, now available for $679 for 8-Core CPU/7-Core GPU/256GB models. Apple’s one-year warranty is included, shipping is free, and each... Read more

Jobs Board

Seasonal Cashier - *Apple* Blossom Mall - J...
Seasonal Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Seasonal Fine Jewelry Commission Associate -...
…Fine Jewelry Commission Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) Read more
Seasonal Operations Associate - *Apple* Blo...
Seasonal Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Read more
Hair Stylist - *Apple* Blossom Mall - JCPen...
Hair Stylist - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.