As described in “About Multitasking on the Mac OS” tasks often need to coordinate with the main application or with other tasks to avoid data corruption or synchronization problems. To coordinate tasks, Multiprocessing Services provides three simple notification mechanisms (semaphores, event groups, and message queues), one complex one (kernel notifications), and critical regions.
Of the three simple notification mechanisms, message queues are the easiest to use, but they are the slowest. Typically a task has two message queues associated with it. It takes messages off an input queue, processes the information accordingly, and, when done, posts a message to an output queue.
Important: You should never use only one instance of a notification mechanism to convey both input and output information, because doing so can easily cause confusion. For example, after posting a request, an application will at some point start waiting for results. If it waits on the same mechanism where the request was posted, the request itself may appear to be the result. The application may then clear the request in the mistaken belief that it was a result and no actual work gets done.
Before notifying a task, your application should make sure that everything the task needs is in memory. That is, you should have created any necessary queues and allocated space for any data the task may require. For each task, your application establishes the parameters of the work that it wants the task to perform and then it must signal the task through either a queue or a semaphore to begin performing that work. The specific work that the task is to perform can be completely defined within a message, or possibly within a block of memory reserved for that task. You can also pass in a pointer to the function that the task should call to perform the work. Doing so allows one task to perform many different types of chores.
Listing 2-4 shows a function that divides up a large amount of data among multiple tasks, placing requests on each task’s request queue and waiting for the results.
Listing 2-4 Assigning work to tasks
OSErr NotifyTasks( UInt32 realFirstThing, UInt32 realTotalThings ) { |
UInt32 i; |
OSErr theErr; |
UInt32 thingsPerTask; |
UInt32 message; |
sWorkParams appData; |
theErr = noErr; |
thingsPerTask = realTotalThings / numProcessors; |
/* Start each task working on a unique piece of the total data */ |
for( i = 0; i < numProcessors; i++ ) { |
myTaskData[i].params.firstThing = |
realFirstThing + thingsPerTask * i; |
myTaskData[i].params.totalThings = thingsPerTask; |
message = kMyRequestOne; |
MPNotifyQueue( myTaskData[i].requestQueue, (void *)message, |
NULL, NULL ); |
} |
/* Now wait for the tasks to finish */ |
for( i = 0; i < numProcessors; i++ ) |
MPWaitOnQueue( myTaskData[i].resultQueue, (void **)&message, |
NULL, NULL, kDurationForever ); |
return( theErr ); |
} |
For each task, it calls MPNotifyQueue to place the pointer to the task’s portion of the data on the task’s request queue. It then calls MPWaitOnQueue to wait for confirmation that the task has completed.
Note: A message queue message is passed to the queue as three pointer-sized parameters. Because the message in Listing 2-4 is only 32 bits long, the remaining two parameters are set to NULL.
If you want to use semaphores or event groups instead of message queues, you would call the following functions to set up, notify, and wait on them, in a manner similar to that shown in Listing 2-4:
MPCreateSemaphore, MPSignalSemaphore, MPWaitOnSemaphore, and MPDeleteSemaphore for semaphores
MPCreateEvent, MPSetEvent, MPWaitForEvent, and MPDeleteEvent for event groups
However, if you use the simpler notification mechanisms, you have to find another way to pass the function pointer to the task. One possibility is to assign the pointer to a field in the task’s task data structure.
To use kernel notifications, you should call the following functions:
MPCreateNotification, MPCauseNotification, MPModifyNotification, and MPDeleteNotification
There is no function for waiting on a kernel notification, as the task would wait on the appropriate subcomponents of the kernel notification (for example, a semaphore and a message queue).
Note that the example in Listing 2-4 will wait forever (kDurationForever) for a message to appear on its result queue. While this method is fine if called from a preemptive task, it can cause problems if called from a cooperative task. If the task takes a significant amount of time to execute, the calling task “hangs” for that time, since it can’t call WaitNextEvent to give other applications processor time. If you want to wait on a task from a cooperative task, your application should post the message and then return to its event loop. From within the event loop it can then poll the result queue using kDurationImmediate waits until a message appears.
If you specify kDurationImmediate for the waiting time for either MPWaitOnQueue, MPWaitOnSemaphore, MPWaitForEvent, or MPEnterCriticalRegion, the function always returns immediately. If the return value is kMPTimeoutErr, then the task generated no new results since the last time the application checked. That is, no message was available, the semaphore was zero, or the critical region was being executed by another processor. If the value is noErr, a result was present and obtained by the call.
Handling Periodic Actions
Notifying Tasks at Interrupt Time
Using Critical Regions
You can use notification mechanisms to do more than simply signal tasks. For example, Listing 2-5 shows a task that uses a semaphore to do periodic actions.
Listing 2-5 Using a semaphore to perform periodic actions
void MyTask(void) { |
MPSemaphoreID delay; |
MPCreateSemaphore(1, 0, &delay); // a binary semaphore |
while(true) |
{ |
DoIt(); // do something interesting |
(void) MPWaitOnSemaphore(delay, 10 * kDurationMillisecond); |
} |
} |
This example uses a semaphore solely to create a delay. After each call to the DoIt function, MyTask waits for a notification that never arrives and times out after 10ms.
You can combine the delaying and notification aspects of a semaphore to add more flexibility as shown in Listing 2-6.
Listing 2-6 Performing actions periodically and on demand
main(void) { |
MPSemaphoreID delay; |
… |
MPCreateSemaphore(2, 0, &delay); |
MPCreateTask(…); |
while(true) { |
// Event loop. |
if ( /* something important happened */ ) |
{ |
MPSignalSemaphore(delay); |
} |
} |
} |
void MyTask(void) { |
while(true) { |
DoIt(); // Do interesting things. |
(void) MPWaitOnSemaphore(work, 100 * kDurationMillisecond); |
} |
} |
In this example, the MyTask task runs essentially as before, except that the main application creates the semaphore. If no signal is sent to the semaphore, the DoIt function in MyTask executes every 100ms. However, in this example the application can signal the semaphore, which unblocks the task and allows the DoIt function to execute. That is, the DoIt function executes whenever the application signals the semaphore, or every 100ms otherwise.
If you want to send a notification to a task from a 68K-style interrupt handler, you can do so using the functions MPSignalSemaphore, MPSetEvent, or MPNotifyQueue. The MPSignalSemaphore and MPSetEvent functions are always interrupt-safe, while the MPNotifyQueue function becomes interrupt-safe if you reserve notifications on the message queue. See the MPSetQueueReserve function description for more information about reserving notifications.
Warning: Aside from these three notification functions, only MPCurrentTaskID , MPBlockClear, MPBlockCopy, MPDataToCode, MPTaskIsPreemptive, and MPYield are interrupt-safe; attempting to call other Multiprocessing Services functions at interrupt time, or at a deferred-task time, may cause a system crash.
If your tasks need access to code that is non-reentrant, (that is, only one task can be executing the code at any particular time), you must designate that code as being a critical region. You do so by calling the function MPCreateCriticalRegion. Doing so returns a critical region ID that you use to identify the region when you want to enter or exit it later. To enter a critical region, the task must call MPEnterCriticalRegion and specify the ID of the region to enter. This function acts much like the functions that wait on message queues and semaphores; if the critical region is not currently available, the task can wait for a specified time for it to become available (after which it will time out).
After the task has completed using the critical region, you must call MPExitCriticalRegion. Doing so “frees” the critical region so that another task that is waiting on it can enter. Note that a task can call MPEnterCriticalRegion multiple times during execution (as in a recursive call) as long as it balances each such call with MPExitCriticalRegion when it leaves the critical region.
Note that the area of code designated as a critical region is not “tagged” as such in any way. You must make sure that your code is synchronized to properly isolate the critical region. For example, if you have a critical region that will be shared by two different tasks, you must create the critical region outside the tasks that will require it and pass the critical region ID to the tasks. This method ensures that, even if multiple instances of a task were created and running, only one could access a particular critical region at a time.
If a task contains more than one critical region, each critical region must have its own unique ID; otherwise, a task entering a critical region may block another task from entering a different critical region.
Last updated: 2007-10-31