49.2 Thread States and Life Cycles

LibC supports three thread interfaces (pthreads, UI, and NKS) and all have their unique states, all threads created from these interfaces through a series of states that you control. The following figure illustrates the potential generic states of a thread and the flow between the states.

Figure 49-4 Thread and Context States

Except for the promotion from waiting on the run queue to execution by the kernel scheduler, the thread reaches each state by your action.

By default, a thread is created and placed in the run queue. It is run as soon as it reaches its turn.The kernel scheduler moves it to a running state, and it stays in that state until one of the following happens:

49.2.1 Thread Yielding

A currently executing thread can relinquish execution explicitly by calling pthread_yield. On NetWare, the thread stops executing and adds itself to the run queue, which permits other threads to run. Normally, by calling an I/O function or other blocking service, a thread implicitly yields the processor. However, there are times when a thread might want to explicitly give up its turn at executing.

In NetWare 5 and above, the underlying yield code measures the frequency of yields and performs them less often where appropriate. Consequently, an explicit yield does not necessarily result in the thread being moved to the end of the run queue. Because the kernel makes the decision, you cannot predict when a yield will be ignored. However, the yield, even when ignored, causes the kernel scheduler to reset the CPU hog time-out so that your thread does not create a CPU hog abend.

In nonpreemptive NetWare versions before 5.1, you had to determine how much work a thread could accomplish without denying other threads their processing time. To certify an NLM, Novell® Labs set a criterion or threshold of how long any one thread was allowed to monopolize the processor, which required you to write explicitly yielding code.

In the current environment, however, explicit yields in NLM programming force processors to perform additional context switches (which are a measure of how much work opportunity the processor has lost) per second. To regain some work opportunity, add yields to your code only in places where it makes sense (or as necessary to pass certification, especially with slower processors).

49.2.2 Joinable Threads and Thread Termination

LibC supports joinable threads. Reading the exit status of a thread is called joining and is significant only at thread termination. A joinable thread persists after termination so that another thread (which may also be joinable) can discover its exit status. By default, threads are created as joinable threads.

Threads that cannot be joined are called detached threads. Although they cannot themselves be joined, detached threads can join any joinable threads. If a thread has no reason to be joined, you should create it as a detached thread.

IMPORTANT:The library cleans up all unjoined threads at unload. However, in a long-lived application, failing to join a thread results in a loss of memory and other resources.

If you have ever explicitly returned a value from main or called exit to communicate the success or failure of a program to the UNIX shell, you have returned a termination status code. A termination state has meaning only if a thread is joinable.

A detached thread is completely gone if either of the following occur:

  • Falls off its last program brace (})

  • Explicitly terminates by calling pthread_exit

However, when these events occur on a joinable thread, the thread's exit status remains, which another thread can read by calling pthread_join.

The thread state named “zombie” represents a terminated, joinable thread. This thread is basically dead—it is not executing, waiting to execute on the run queue, or suspended in any useful sense. However, the zombie thread is not completely destroyed. It must first communicate its impending “death” to another thread.

You do not need to wait for a joinable thread to die before joining it. The joining thread calls pthread_join, then gets the exit status when the joinable thread terminates. If the calling thread passes 0 as the identity of the thread to be waited on, pthread_join returns the identity and status of the next application thread to die.

Joining has many useful applications. For example, resource management for an application might be easier if you know when its threads die. Because an application must manage its own contexts, it might have some difficulty knowing when those contexts can be freed. Joining might be a solution (as would not setting the do-not-destroy flag when you create the thread).

Joining also provides a simple means of synchronization. However, joining a thread to synchronize code or data should not supplant the use of condition variables or other explicit synchronization objects because those mechanisms synchronize more efficiently than the relatively expensive cost of creating and terminating threads (see Synchronization Concepts).