[Develope]/Network

porting multithread => Max OS X(posix)

하늘을닮은호수M 2005. 6. 30. 13:51
반응형

Porting Multithreaded Applications from Win32 to  MacOSX

When writing multithreaded applications, MacOSX offers a choice of programming interfaces that give you varying degrees of control. Which API you choose depends on a number of factors, including the level of multithreading support required, your familiarity with object-oriented programming (specifically, OOP with Objective-C), your cross-platform objectives, and of course, your schedule.

If you have a Win32-based application that you want to port to MacOSX and you are primarily a Windows programmer, read this article to get a solid grounding in how MacOSX handles multithreading.

MacOSX offers application-level multithreading support in three different API packages:

  • POSIX threads, or pthreads
  • Cocoa threads
  • Carbon Multiprocessing Services

The C-based POSIX thread library, commonly referred to as ‘pthreads’, is the lowest level of the three. In fact, both Cocoa threads and the Carbon Multiprocessing Services are implemented using POSIX threads. The POSIX thread library offers the most granular level of multithreading support. Consequently, it is also the most complex and most difficult of the three libraries to use.

Cocoa threads are part of the Foundation framework. The Cocoa threading package is implemented in Objective-C, and presents an object-oriented interface to the pthread library. The NSThread, NSLock, and NSConditionLock classes provide a high-level abstraction of the thread management and synchronization functions in the pthread library. Depending on the complexity of your Win32 code, Cocoa’s automatic thread management might be exactly what you are looking for.

The Carbon Multiprocessing Services are a procedural, C-based abstraction of the pthread library. The programming model and terminology of the Carbon threading package is very similar to the native Win32 multithreading API. If you are considering whether to port your application using Cocoa or Carbon, this similarity could tip the scales in favor of Carbon.

When choosing a threading package, you must also consider your long-term goals. Which platforms will you be targeting in the future? While the pthread library is the most complex, it does have some important advantages that should weigh heavily in your decision. First, the pthread library is an industry standard library available on many UNIX platforms, including Linux and FreeBSD. Perhaps most importantly, you can call the pthread library regardless of whether you create a Cocoa or Carbon application on MacOSX.

It’s true that pthreads are not likely to ever be supported natively on Microsoft Windows. However, there are open source alternatives that allow you use pthreads in a Win32 environment. One such project is pthreads-win32, which implements most of the thread-related parts of the POSIX 1003.1-2001 standard on the Win32 platform.

Note: If you visit the pthreads-win32 project site, be sure to follow the links to the conformance page for a complete list of implemented functions.

If your schedule allows for the added complexity, selecting the pthread library is a strategic decision that could open up more target platforms and therefore a wider market for your application.

The remainder of this article will show you how commonly used Win32 multithreading APIs map to the three MacOSX alternatives, pthreads, Cocoa, and Carbon Multiprocessing Services.

Prerequisites

This article assumes the following:

  • You have an existing multithreaded Windows application you wish to port to MacOSX.
  • You are familiar with multithreading concepts in general, and with the Win32 multithreading API.
  • If you are considering the Cocoa frameworks for your MacOSX application, you should read the Introduction to the Objective-C Programming Language to familiarize yourself with some of the Cocoa terminology presented here.

Creating a Thread

Creating a thread is conceptually similar in all of the threading packages. Broadly speaking, you write a routine that will be run on the new thread of execution, and you supply the address of this routine to the thread creation functions. From there, the level of control and mapping thread parameters to their MacOSX equivalents varies widely.

Creating a Thread on Win32

Recall the signature of the Win32 CreateThread function:

HANDLE CreateThread(        LPSECURITY_ATTRIBUTES lpThreadAttributes, // Determines if the handle can be                                                  // inherited by child processes.        SIZE_T dwStackSize,                       // The thread’s stack size in bytes.        LPTHREAD_START_ROUTINE lpStartAddress,    // Address of the thread function.        LPVOID lpParameter,                       // A parameter to pass to the                                                  // thread function.        DWORD dwCreationFlags,                    // Create thread in suspended state?        LPDWORD lpThreadId                        // Address to receive the thread id.);

Let’s look at how this function and its parameters map to the three threading libraries available on MacOSX .

Creating a Thread Using POSIX Threads

You use the pthread_create function to create a POSIX thread. The function takes four parameters:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

Not all of the Win32 parameters have a pthreads counterpart. As with all pthreads functions, the return value is a status code. The value of the thread identifier is returned in the parameter pthread_t thread.

The CreateThread parameter lpThreadAttributes allows you to specify whether or not the thread handle can be inherited by child processes. There is no equivalent for this parameter on MacOSX.

On Win32, the signature of your thread function is

DWORD ThreadFunc(LPVOID lpParameter);

In pthreads the signature is

void *threadFunc(void *arg);

It’s the same concept with a slightly different signature. In both cases the thread routine takes one parameter and returns one value. The start_routine and arg parameters specify the address of the thread function and its parameter, respectively. Retrieving the thread's exit status is discussed in the section Handling Thread Termination in pthreads.

The dwCreationFlags parameter of CreateThread allows you to spawn the thread in a suspended state. The thread will not run until you call the ResumeThread function. There is no equivalent to this in the pthread_create function.

The Carbon Multiprocessing Services package does provide the ability to create a thread in a suspended state. This is covered in the section Creating a Thread Using Carbon Multiprocessing Services.

On Win32, stack size of the thread is set in the CreateThread call. Using pthreads, stack size is set using thread attribute functions. Notice the attr parameter in the pthread_create function. You can pass NULL to this parameter to accept the default thread attributes, or you can create an attribute object and use it to set additional thread parameters.

Setting POSIX Thread Attributes

To set thread attributes in pthreads you must create an attribute object of type pthread_attr_t and use the pthread_attr_set * functions to set the attribute values. For example, you could use this code to set the thread’s stack size attribute

pthread_t threadId;pthread_attr_t stack_size;  pthread_attr_init(&stack_size);  pthread_attr_setstacksize(&stack_size, 1048576);  pthread_create(&threadId, &stack_size, threadFunc, NULL);  pthread_attr_destroy(&stack_size);

There are many pthread_attr_set functions. For example, you use thread attributes to set the thread’s scheduling policies. You can allocate an attribute object and call all the pthread_attr_set functions you require before passing the object along to pthread_create. You are not required to keep the attribute object around after you have called pthread_create.

Creating a Thread Using Carbon Multiprocessing Services

In Carbon Multiprocessing terms, a task is one preemptively scheduled thread of execution. The Carbon Multiprocessing function to create a new task is quite similar to the Win32 CreateThread function.

OSStatus MPCreateTask(          TaskProc entryPoint,          void *parameter,          ByteCount stackSize,          MPQueueID notifyQueue,          void *terminationParameter1,          void *terminatinoParameter2,          MPTaskOptions options,          MPTaskID *task);

Note that this article will continue using the word thread to refer to a Carbon Multiprocessing task.

Again, you must specify a thread function and its one parameter in the entryPoint and parameter arguments. The signature of your thread function must be

OSStatus taskFunc(void *parameter);

Like its Win32 counterpart, the thread stack size is passed directly to the MPCreateTask function. Passing a zero in this parameter results in a default stack size of 4KB.

Skipping the notifyQueue and termination parameters for the moment, the options parameter allows you to duplicate some of the behavior of the Win32 dwCreationFlags. You can create the thread in a suspended state by passing the value kMPCreateTaskSuspendedMask in the options parameter. However, creating a thread in a suspended state is not recommended, as unexpected signals can wake the thread before you are ready for it to run. Instead, it is better to use a semaphore to block the newly created thread early in its execution (i.e. within the thread's own thread function). When you are ready for the thread to run, signal the semaphore and the thread function will wake up and continue. See the MPTaskOptions enumeration for other thread creation options.

The unique thread ID is passed back in the task argument. You will use this value in all subsequent calls to manage the new thread.

Carbon Multiprocessing Notification Queues

The Carbon Multiprocessing API offers traditional thread synchronization tools such as semaphores and critical regions. It also contains functions to create and manage message queues - resources you can use to communicate between threads. Before calling MPCreateTask you must create a notification queue with the MPCreateQueue function:

OSStatus MPCreateQueue(MPQueueID *queue);

A message consists of three 32-bit values having programmer-defined semantics. When a thread exits or is terminated, a message is posted to the notification queue specified in the MPCreateTask function. The first two 32-bit values that comprise this message are passed to MPCreateTask in the terminationParameter1 and terminationParameter2 arguments. Either or both of these arguments can be NULL if you don’t need to use them. The third value is the thread’s exit status code. Retrieving the thread’s exit status is discussed further in the section Handling Thread Termination in Carbon Multiprocessing Services.

Messages are posted to a queue with the MPNotifyQueue function:

OSStatus MPNotifyQueue(MPQueueID queue, void *param1, void *param2, void *param3); 

To wait for a message on a queue, you call the MPWaitOnQueue function:

OSStatus MPWaitOnQueue(MPQueueID queue, void **param1, void **param2, void **param3, Duration timeout);

The timeout argument gives you the flexibility of returning immediately, waiting forever, or specifying a finite time in either milliseconds or microseconds. See the topic Timer Duration Constants for information on the possible values.

Multiple threads can wait on the same message queue. In this case, messages are dispatched to the waiting threads in a first in, first out manner. The first thread to call MPWaitOnQueue will be unblocked when a message is posted to the queue. The message is then removed from the queue, and the next waiting thread will be unblocked when a message arrives in the queue.

More information on Carbon Multiprocessing message queues can be found in the Carbon Reference Library topic, Creating and Handling Message Queues.

Creating a Thread Using Cocoa

To create a thread with Cocoa, you don’t instantiate an object of the NSThread class. Instead, you call a class (static) method of NSThread, +detachNewThreadSelector:toTarget:withObject.

The method calls the method selector of the object passed in the toTarget parameter. The withObject argument allows you to pass a parameter to the thread method.

The thread method itself must be declared as a class method that takes one parameter of type id, and that has no return value, as shown here:

+void threadMethod:(id)arg;

As indicated by its name and lack of a thread method return value, the +detachNewThreadSelector:toTarget:withObject method creates a detached POSIX thread. Detatched threads are covered in the section Handling Thread Termination in pthreads.

None of the other Win32 thread creation parameters are exposed by the NSThread class. NSThread does expose methods to query and set a thread’s priority. On Win32, you get and set thread priority using the GetThreadPriority and SetThreadPriority functions.

The following code snippet shows how you create a thread using NSThread. This code is part of a programmer-defined class called CLDispatcher. The thread method is called listenerThreadWithObj. The thread method is passed one parameter, which is an instance of the CLDispatcher class itself.

-(void) startListener{    [NSThread detachNewThreadSelector:@selector(listenerThreadWithObj:)               toTarget:[CLDispatcher class] withObject:self];}// This is the thread method+(void) listenerThreadWithObj:(id)obj{   // ...Thread code...}

Creating a thread using NSThread automatically puts your application and the Foundation framework into multithreaded mode. However, you can also use the pthreads library directly, without using the NSThread class. If you take this approach you must put the framework into multithreaded mode by hand. A very simple way to so this is to use NSThread to spawn a thread, which immediately exits.

Thread Synchronization

Win32 Synchronization Review

The three main synchronization tools available on Win32 are:

  • Critical sections and mutexes
  • Semaphores
  • Events

Mutexes have direct counterparts in pthreads. The Cocoa threading package provides mutual exclusion with the NSLock class. The Carbon Multiprocessing package implements critical regions, which map directly to Win32 critical sections.

The pthreads library implementation on MacOSX currently provides POSIX named semaphores. The Carbon Multiprocessing API provides the closest mapping to the Win32 implementation of semaphores.

The Win32 event object has no direct analog in either the pthreads or the Cocoa packages. However, both pthreads and Cocoa threads implement condition variables (Cocoa does so in a restricted form), which are conceptually similar. The Carbon Multiprocessing package does implement events in a way that is similar to Win32.

Thread Synchronization Tools with pthreads

Using Mutexes

To create a mutex in pthreads, declare a variable of type pthread_mutex_t. Before using the mutex you must initialize it. There are two ways to do this.

The first way is to statically initialize the mutex using the PTHREAD_MUTEX_INITIALIZER macro. For example:

pthread_mutex_t myMutex;   myMutex = PTHREAD_MUTEX_INITIALIZER;

The second way to initialize a mutex variable is to use an attribute object and the pthread_mutex_init function. You use the pthread_mutexattr_init and pthread_mutexattr_set group of functions to initialize and configure the attribute object prior to handing it off to pthread_mutex_init. This is similar in concept to the pthread_attr_set and get functions described in the section Setting POSIX Thread Attributes.

The pthread_mutexattr_init function takes the address of a pthread_mutex_t variable:

int pthread_mutextattr_init(pthread_mutexattr_t *attr);

You then set mutex attributes using one of the functions:

int pthread_mutexattr_setprioceiling(pthread_mutex_attr_t *attr, int prioceiling);int pthread_mutexattr_setprotocol(pthread_mutex_attr_t *attr, int protocol);int pthread_mutexattr_setpshared(pthread_mutex_attr_t *attr, int pshared);

Finally, you pass the attribute object to the pthread_mutex_init function:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *attr);

If you wish to accept the default mutex attributes, you can simply pass a NULL value to pthread_mutex_init.

Note there are also three corresponding pthread_mutexattr_get functions you can use to examine a mutex attribute object:

int pthread_mutexattr_getprioceiling(pthread_mutex_attr_t *attr, int *prioceiling);int pthread_mutexattr_getprotocol(pthread_mutex_attr_t *attr, int *protocol);int pthread_mutexattr_getpshared(pthread_mutex_attr_t *attr, int *pshared);

On Win32 you gain access to a mutex using a Wait function (such as WaitForSingleObject). When your code is finished with the mutex you call ReleaseMutex.

The pthread equivalent functions are:

int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);

The pthread_mutex_lock function blocks the calling function until the mutex is unlocked with pthread_mutex_unlock. When more than one thread is waiting on the same locked mutex, the pthread library uses the waiting threads’ scheduling priority to determine which thread should be granted access to the mutex when it is released.

If you wish to attempt to lock a mutex without blocking, you can use the pthread_mutex_trylock function:

int pthread_mutex_trylock(pthread_mutex_t *mutex);

If the mutex is unlocked, pthread_mutex_trylock will lock it and return zero. Otherwise, it will return immediately, with a return code of EBUSY. This allows you to test the lock state of a mutex, in a similar way to the Win32 method of calling a Wait function with a timeout value of zero.

When you are finished using a mutex, you must deallocate it with a call to pthread_mutex_destroy:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

This function frees the mutex, so it cannot be used again in any call to a pthread_mutex function.

Using POSIX Named Semaphores

MacOSX supports POSIX named semaphores. While the other mechanisms discussed in this article are for use in the threads of a single process, named semaphores can be used to synchronize multiple processes. A named semaphore exists in the file system (though no visible filesystem entry exists). It can therefore be opened by any process that knows its name.

Note that you must include the semaphore.h header file to use named semaphores.

To create or open a named semaphore, use the function

sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int initialValue);

The oflag argument can be one of these three values:

  • 0: Open the semaphore. The status code SEM_FAILED is returned if the semaphore does not exist.
  • O_CREAT: Create and initialize the semaphore.
  • O_CREAT | O_EXCL: Returns the code SEM_FAILED if the semaphore already exists.

If O_CREAT is specified, the next two arguments are required (they can be omitted otherwise). The mode argument specifies the permission bits for the semaphore. The mode constants are found in the sys/stat.h header file:

S_IRUSRuser read permission
S_IWUSRuser write permission
S_IRGRPgroup read permission
S_IWGRPgroup write permission
S_IROTHother read permission
S_WOTHother write permission

The initialValue argument is the semaphore’s initial count value. It cannot be greater than the defined constant SEM_VALUE_MAX.

To close a semaphore, use the function

int sem_close(sem_t *sem);

To remove a semaphore from the file system, use the function

int sem_unlink(const char *name);

From here, POSIX semaphores behave, at least conceptually, in a way similar to Win32 semaphores. The semaphore’s count value is incremented when the resource it guards becomes available, and decremented when a thread has been granted access to the resource. Four functions are used to manage a semaphore:

int sem_wait(sem_t *sem);int sem_trywait(sem_t *sem);int sem_post(sem_t *sem);int sem_getvalue(sem_t *sem, int *value);

All four functions return 0 on success, or -1 if an error occurs.

The sem_wait function blocks the caller until the count value is greater than zero. Then, the caller is released and the count value is decremented. You can use the sem_trywait function to test the count value. The sem_trywait function does not block the caller. Instead it immediately returns a value of EAGAIN if the value is currently zero.

To increment the value of a semaphore, you call the sem_post function. One difference between a semaphore and a mutex is that a thread acquiring a semaphore with sem_wait or sem_trywait need not be the same thread that increments the value with sem_post. A mutex must be locked and unlocked by the same thread.

Another important feature of semaphores is that a thread can call the sem_post function at any time, regardless of whether there are other threads waiting in a sem_wait call. The fact that the semaphore’s value was incremented is retained, even if there are no waiting threads. Contrast this with condition variables, where the fact that the condition is signaled is lost if there are no waiting threads. Condition variables are discussed in the section Using pthreads Condition Variables.

Using Pthreads Condition Variables

A condition variable is used by a thread to suspend its own execution until an expression involving shared data reaches a desired state. The Win32 multithreading API does not have a native implementation of condition variables. There have been many implementations of the condition variable model using available Win32 synchronization tools such as events. The pthreads library contains a native implementation of condition variables. While condition variables are still complex in nature, the fact that they are supported natively in the library could help you simplify difficult synchronization code.

A pthreads condition variable is always used in conjunction with a mutex. When you need to make a thread wait on a condition variable, you must first lock the mutex, and then wait on the condition variable. The flow of events is as follows:

  1. Thread A needs to wait on the condition variable. It locks the mutex associated with the condition variable with pthread_mutex_lock.
  2. Thread A calls pthread_cond_wait, which takes the condition variable and its associated mutex as parameters. The pthread_cond_wait function releases the associated mutex so that other threads can lock it and change the shared data. Thread A is now waiting until the condition variable has been signaled by another thread.
  3. Thread B needs to change the value of the shared data. It locks the associated mutex and makes the change.
  4. Thread B signals thread A by calling the pthread_cond_signal function. Thread B could also signal all waiting threads by calling pthread_cond_broadcast.
  5. After signaling, thread B releases the associated mutex with pthread_mutex_unlock.
  6. Thread A “wakes up” from the pthread_cond_wait call. Upon return from pthread_cond_wait, the associated mutex has been locked on thread A’s behalf (by the pthread library), so that the shared data can be safely examined.

When a thread waiting on a condition variable wakes up, it should immediately check the shared data to be sure it is in the required state. This seems redundant at first glance but it is necessary for three reasons:

  • In the case where multiple threads are waiting on the same condition variable, it’s possible that another thread was scheduled to run first, and has already changed the shared data.

This is difficult to grasp at first, but recall that there are two parts to a condition variable: the condition variable itself, and its associated mutex.

When a condition variable is signaled, its associated mutex must be reacquired by the library. When multiple threads are waiting, the library uses the threads’ scheduling priority to determine which thread will be granted the mutex lock. Other threads are then put into a queue of threads that are waiting to acquire the mutex. Therefore, just because our thread wakes up when the condition variable is signaled, it does not mean it was the first to reacquire the mutex.

  • It’s always possible that the condition was signaled in error, due to a logic bug in the signaling thread.
  • The pthread standard does not prohibit the underlying threading implementation from waking up a waiting thread.

Like mutex variables, condition variables can be created and initialized either statically or dynamically. The following code statically initializes a condition variable:

pthread_cond_t myConditionVariable;   myConditionVariable = PTHREAD_COND_INITIALIZER;

To dynamically create and initialize a condition variable, you use the pthread_cond_init function. If you want to assign custom attributes to the condition variable, use the pthread_condattr_init, and related functions. The pthread_condattr_init function takes the address of a pthread_condattr_t variable:

int pthread_condattr_init(pthread_condattr_t *attr);

There is only one condition variable attribute, which governs whether or not the variable can be used by threads in other processes. The functions

int pthread_condattr_setpshared(pthread_condattr *attr, int pshared);int pthread_condattr_getpshared(pthread_condattr *attr, int *pshared);

set and retrieve the attribute, respectively. The pshared parameter has the value PTHREAD_PROCESS_SHARED, or PTHREAD_PROCESS_PRIVATE.

You pass the attribute variable to the pthread_cond_init function.

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);

To accept default condition variable attribute settings, pass a NULL value to pthread_cond_init.

There are two functions for waiting on a condition variable:

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

The timedwait function allows you to specify the number of seconds to wait in a timespec structure (defined in time.h).

Two functions allow for signaling one thread, or all waiting threads, respectively:

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

When you are finished using a condition variable, call pthread_cond_destroy:

int pthread_cond_destroy(pthread_cond_t *cond);

This will delete the condition variable so that it cannot be used again in any pthread_cond function.

Using pthread_join

To suspend execution of one thread until another thread terminates, you can call the Win32 WaitForSingleObject with the thread’s handle. When the thread exits, its handle becomes signaled and WaitForSingleObject returns.

The pthread equivalent is the pthread_join function. pthread_join waits for the specified thread to exit. The thread’s exit code is retrieved through a parameter passed to pthread_join. The signature of pthread_join is:

int pthread_join(pthread_t thread, void **value_ptr);

Retrieving the thread’s return value is discussed in the section Handling Thread Termination in pthreads.

Thread Synchronization with Carbon Multiprocessing

The Carbon Multiprocessing thread package provides four ways to synchronize access to shared data:

  • Critical regions
  • Message notification queues
  • Semaphores
  • Events (called event groups)

Carbon Critical Regions

The four Carbon functions for handling critical regions are:

OSStatus MPCreateCriticalRegion(MPCriticalRegionID *criticalRegion);OSStatus MPDeleteCriticalRegion(MPCriticalRegionID criticalRegion);OSStatus MPEnterCriticalRegion(MPCriticalRegionID criticalRegion, Duration timeout);OSStatus MPExitCriticalRegion(MPCriticalRegionID criticalRegion);

These functions map to the Win32 functions, InitializeCriticalSection, DeleteCriticalSection, EnterCriticalSection, and LeaveCriticalSecion, respectively.

The MPEnterCriticalRegion function allows you to specify a timeout value. Using a timeout value of kDurationImmediate results in behavior similar to the Win32 function TryEnterCriticalSection, See the topic Timer Duration Constants for information on how to use the timeout parameter.

As on Win32, the critical region entry function can be called multiple times from within the critical section. Each call to MPEnterCriticalRegion must be balanced with a corresponding call to MPExitCriticalRegion.

Carbon Multiprocessing Message Notification Queues

Carbon applications can use notification queues as a simple way to pass messages between threads. A message consists of three 32-bit values, which are not interpreted by the system. Their meaning is completely programmer-defined and any or all of the three values can actually be NULL.

The Carbon functions for creating and managing notification queues are shown in this article in the sections Carbon Multiprocessing Notification Queues, and Handling Thread Termination in Carbon Mulitprocessing Services. Still more information on message notification queues can be found in the Carbon Reference Library topic, Creating and Handling Message Queues.

Carbon Multiprocessing Semaphores

The Carbon multiprocessing semaphore functions are quite similar to their Win32 counterparts. When you create the semaphore, you specify the maximum and initial count values, just as you do on Win32. The four Carbon Multiprocessing semaphore functions are:

OSStatus MPCreateSemaphore(MPSemaphoreCount maximumValue, MPSemaphoreCount initialValue, 	                       MPSemaphoreID *semaphore);OSStatus MPDeleteSemaphore(MPSemaphore ID semaphore);OSStatus MPSignalSemaphore(MPSemaphoreID semaphore);OSStatus MPWaitOnSemaphore(MPSemaphoreID semaphore, Duration timeout);

Like the Win32 ReleaseSemaphore function, Carbon’s MPSignalSemaphore increases the semaphore’s count value by one and unblocks the oldest thread waiting in a call to MPWaitOnSemaphore. When a call to MPWaitOnSemaphore completes without timing out, the semaphore’s count is decremented by one. The thread can then proceed, and when access to the semaphore is no longer needed it calls MPSignalSemaphore.

When the semaphore’s count reaches zero, a thread that calls MPWaitOnSemaphore will be blocked until the semaphore’s count is again greater than zero, and the thread is at the head of the queue of waiting threads. The timeout argument allows you to wait indefinitely, wait for a finite period of time, or to not wait at all (i.e. test the semaphore’s count without blocking). See the topic Timer Duration Constants for information on how to use the timeout parameter.

Carbon applications can use the macro

MPCreateBinarySemaphore(MPSemaphoreID *semaphore)

to create a binary semaphore, which behaves like a mutex. The macro expands to a call to MPCreateSemaphore, creating a semaphore with its maximum and initial values both set to one.

Carbon Multiprocessing Event Groups

A Carbon event group is conceptually similar to an auto-reset event in Win32. An event group has the type MPEventFlags, and is an aggregate of individual, 1-bit flags. The flags in an event group are simply binary values indicating whether the event has occurred or not.

The four Carbon functions for creating and managing event groups are:

OSStatus MPCreateEvent(MPEventID *event);OSStatus MPDeleteEvent(MPEventID event);OSStatus MPSetEvent(MPEventID event, MPEventFlags flags);OSStatus MPWaitForEvent(MPEventID event, MPEventFlags *flags, Duration timeout);

On Win32, the SetEvent function puts an event handle into a signaled state. The Carbon MPSetEvent function is similar, except with it you can indicate the occurrence of multiple events. You do this by setting bits in the flags parameter. Calling MPSetEvent causes the flags to be logically OR’d with the current flags in the event group.

Threads wait for event flags to be set by calling the MPWaitForEvent function. When MPWaitForEvent returns without timing out, all of the flags comprising the event group are returned in their current state (there is no way to poll for individual flags). All of the flags are then cleared, similar to an auto-reset event on Win32. As with Carbon semaphores, multiple threads waiting on an event group are queued in first in, first out order. When flags are set with MPSetEvent, the first (i.e. the oldest) waiting thread in the queue is unblocked.

The timeout argument of the MPWaitForEvent function allows you to wait indefinitely, or for a finite amount of time. You can also test the state of the event group without blocking. See the topic Timer Duration Constants for information on how to use the timeout parameter.

Thread Synchronization with Cocoa Threads

The thread synchronization tools offered by Cocoa are limited compared to those of pthreads and Carbon Multiprocessing. As a tradeoff, they offer much more in the way of automatically managed attributes and are therefore much easier to use. Cocoa offers two synchronization classes: NSLock, and NSConditionLock.

Using the NSLock Class

The NSLock class adopts (implements) the NSLocking protocol, which contains two methods, lock and unlock. To use the class, simply allocate and initialize an instance, and then surround the block of code you wish to protect with lock and unlock calls, as illustrated here:

NSLock *myLock;  myLock = [[NSLock alloc] init];  [mylock lock];   // protected code here  [mylock unlock];

The NSLock class contains two additional methods, lockBeforeDate, and tryLock. The lockBeforeDate method accepts an NSDate object and will timeout if the lock cannot be acquired before the specified date and time.

The tryLock method can be used to test the state of a lock. The method returns immediately with a value of YES or NO, depending on whether the lock was acquired. If tryLock returns a value of YES, the lock was acquired and you must follow up with a call to unlock to release it.

Using the Objective-C @synchronized Directive

If you require a simple Win32-style critical section, you don’t have to dig into the NSLock class at all. The Objective-C language has a directive called @synchronized that you can use instead.

The @synchronized directive takes one parameter, which can be any Objective-C object (i.e. it is of type id). The parameter is the object you wish to have exclusive access to in the code block protected by the directive. The @synchronized directive introduces a code block, as shown in the following example:

-(void) aMethod:(id)anObject{  @synchronized(anObject) // Exclusive access to anObject requested here...  {     // Exclusive access to anObject has been granted...     [anObject modify];  }                       // Exclusive access to anObject relinquished here.}

The mutex associated with the block is automatically released on exit from the code block.

For more information on the @synchronized directive, see Synchronizing Thread Execution in The Objective-C Programming Language.

Using the NSConditionLock Class

The NSConditionLock class implements a notification mechanism somewhat similar to a Win32 manual-reset event. The class contains methods that let you synchronize participating threads by waiting and locking when user-defined conditions are met.

User-defined conditions are integers, and the NSConditionLock class contains methods such as initWithCondition and lockWhenCondition to initialize the lock in a particular state, and to wait on the lock until the given condition is achieved. After acquiring an NSConditionLock, you can use the unlockWithCondition method to release the lock in a new state. Other threads waiting on the lock with the lockWhenCondition method will then be awakened.

The NSConditionLock class is essentially a pthreads condition variable with the condition test always being of the form:

(condition == value)

where condition is the current value of the lock, and value is the user-defined state you are waiting for. The value is passed as a parameter to the lockWhenCondition method.

Thread Termination

Thread Termination on Win32

When a thread function completes, its status code is returned by either:

  • Directly returning a value of type DWORD from the thread function
  • Calling the ExitThread function with the thread’s status code

To retrieve the thread’s status code, you use the GetExitCodeThread function.

In the most extreme cases, you can use the TerminateThread function to stop a thread with no questions asked.

These exit strategies map closely to both the pthreads and Carbon Multiprocessing APIs. The Cocoa threading classes behave a bit differently. These differences will be discussed below.

Handling Thread Termination in pthreads

Conceptually, returning the exit status of a thread is exactly the same in the pthreads library as it is on Win32. You can either:

  • Return a value of type void * directly from the thread function
  • Call the pthread_exit function with the thread’s status code

Retrieving the thread’s status code however, is handled a little bit differently. In the pthreads library, a thread is said to be either joinable, or detached.

A joinable thread is one whose thread identifier can be passed to the pthread_join function. The pthread_join function waits for the specified thread to terminate. It also takes a parameter in which the library will store the thread’s status code. By default, a thread is joinable when it is created.

A detached thread cannot be used in a call to pthread_join, and therefore it has no return status code. There are two ways to create a detached thread:

  • Specifying thread attributes when you call pthread_create
  • Calling the pthread_detach function after the thread has been created

You can return integral values from a thread, as long as your status codes do not conflict with the reserved value PTHREAD_CANCELED. Of course, you will also have to cast those values to their appropriate types, as the status code is a void * as far as pthreads is concerned. Note that avoiding the PTHREAD_CANCELED value is a problem similar to the mistake of accidentally returning the reserved STILL_ACTIVE value on Win32.

Canceling a Thread in pthreads

As on Win32, terminating a thread prematurely is fraught with peril. You should always try to structure your code so this case can be avoided. This article will not cover thread cancelation in great detail. Suffice it to say that the pthreads library gives the thread itself substantial control over its own cancelation. Indeed, a thread can elect to make itself not be cancelable at all. A thread can even switch in and out of a cancelable state as it runs.

n the pthreads library, threads have both a cancellation state, and a cancellation type. The cancellation state determines whether the tread can be cancelled or not. It can take the values:

  • PTHREAD_CANCEL_DISABLE
  • PTHREAD_CANCEL_ENABLE

You set the cancellation state with the function

int pthread_setcancelstate(int state, int *oldstate);

The thread’s cancellation type determines when the thread can be canceled. It can take the values:

  • PTHREAD_CANCEL_ASYNCHRONOUS
  • PTHREAD_CANCEL_DEFERRED

The cancellation type is set by the function

int pthread_setcanceltype(int type, int *oldtype);

By default a thread is cancelable, and its cancellation type is set to PTHREAD_CANCEL_DEFERRED.

The asynchronous cancellation type means the thread can be cancelled at any point in its execution. The usual red flags and warnings apply in the pthreads library; terminating a thread can wreak havoc with other threads in the process. The pthreads library does give you a way to specify cleanup routines, which are run when a thread is cancelled. This will be discussed shortly.

The deferred cancellation type means the thread can only be cancelled when it is at a certain point in its execution. There are two types of cancellation points:

  • Automatic

There are three automatic cancellation points:

  • pthread_cond_wait
  • pthread_cond_timedwait
  • pthread_join

The thread can be cancelled while it is blocked in any of these three functions.

  • Programmer-defined

A programmer-defined cancellation point is inserted with the function pthread_testcancel. The call has no effect if there is no cancellation request pending. You can insert a call to pthread_testcancel at any point in the code where a termination is safe, such as prior to acquiring a lock or before a lengthy operation.

Thread Cleanup Stacks

Cleanup stacks give each thread a chance to execute a set of final routines when it is terminated. You add a function to the thread’s cleanup stack with the function

void pthread_cleanup_push(void (*routine)(void *), void *arg);

The signature of a cleanup routine is

void cleanupRoutine(void *);

The arg parameter of pthread_cleanup_push will be passed to the cleanup function.

Cleanup functions are executed in last-in-first-out order, as implied. Note that cleanup routines are executed even when a thread exits cleanly with pthread_exit.

The pthread standard requires that every call to pthread_cleanup_push within a lexical scope be paired with a corresponding call to the function

void pthread_cleanup_pop(int execute);

The execute parameter gives you a chance to run the cleanup routine currently at the top of the cleanup stack. If you pass a value of 1, the routine will be executed. If you pass a value of zero, the routine will be removed from the stack but it will not be executed.

Handling Thread Termination in Carbon Mulitprocessing Services

To terminate a thread in application using Carbon Multiprocessing Services, you can:

  • Return from the thread function
  • Call the MPExit function from within the thread function
  • Call the MPTerminateTask function from another thread

When a thread function exits, its return value is posted to the message notification queue that was passed to the MPCreateTask function. Similarly, the MPExit function, which has the signature:

void MPExit(OSStatus status);

will post the status parameter to the thread’s notification queue.

To synchronize one thread with the completion of another thread (similar to calling WaitForSingleObject on a thread handle in Win32), you call the MPWaitOnQueue function. The signature of MPWaitOnQueue is:

OSStatus MPWaitOnQueue(MPQueueID queue, void **param1, void **param2, void **param3);

Returning an exit code from a thread function works as follows. Recall that the MPCreateTask function takes two parameters of type void * ,called terminationParameter1, and terminationParameter2. These two parameters correspond to the param1 and param2 arguments of the MPWaitOnQueue function. Their programmer-defined values should reflect the fact that a thread has exited. The third argument of MPWaitOnQueue, param3, is the thread function’s return value, or the value passed to MPExit.

As with every multithreading package, asynchronously terminating a thread can cause serious problems and should be avoided if possible. Resources created in a thread are not released when the thread is terminated, and synchronization mechanisms such as critical regions are left in an indeterminate state.

That said, there are cases where thread termination is relatively safe. For example, if a thread is known to be waiting on a resource such as a queue or a semaphore, you could use MPTerminateTask. The correct way to use this function is to call it and when wait on the task’s notification queue until a message arrives. The signature of MPTerminateTask is:

OSStatus MPTerminateTask(MPTaskID task, OSStatus terminationStatus);

The terminationStatus argument is again returned as param3 in the call to MPWaitOnQueue. The thread calling MPTerminateTask and waiting on the queue should then handle cleanup of resources, since the terminated thread itself cannot.

Handling Thread Termination with Cocoa Threads

A thread created with the Cocoa NSThread class is detached, meaning its return status code is not retrievable in the same way as with Win32 or in the pthreads library.

The fact that a Cocoa thread is detached does not mean the thread cannot indicate a return status code, however. The status code could easily be a member variable of a class. You could pass the class as a parameter to the thread function.

A notification of type NSThreadWillExitNotification is sent to all registered observers before the thread function terminates. This allows observers to read the thread’s exit code. For more information on Notifications, see the topic Introduction to Notifications.

Thread Local Storage

The Win32 thread local storage (TLS) API maps fairly well to both pthreads and the Carbon Multiprocessing APIs. The Cocoa NSThread class maintains one instance of the NSMutableDictionary class (a table of key-value pairs) that you can use for storing thread-specific data.

In all three cases the concept is basically the same as on Win32: if you need to store a piece of interesting information about a thread, you request a unique key and then call functions to get or set the value

반응형