Salsa - Threads Topics Nachos Synchronization

Never call a destructor function directly in your code. It is called automatically when you call the delete operator.



[ Creation| Fork| Switching| Yield| Finish| Sleep ]
[ Source code in code/threads | Lab| Quiz ]

What are they?
A traditional process has a single thread of control and a single program counter. Modern operating systems provide multiple threads of control within a process - these are called Threads or lightweight processes. All threads within a process share the same address space. This means that a thread shares its code and data section with other threads. Each, however, has its own program counter, stack and register set. The program counter determines which instruction the thread is currently executing. This may or may not be the same as that for other threads. The register set is needed because when threads are suspended, their state must be saved. Upon resumption, this state is loaded into the machine registers.

Like traditional processes, threads can be in different states - running, ready or blocked .

Why have them?
To see why multiple threads of control within a process are useful, consider what a web browser such as Netscape must do to load a web page containing multiple images. For each image, it must set up a separate connection to the page's home site and request the image. By having multiple threads within the browser process, the images can be requested in parallel with the user scrolling through the content that has already been received, greatly enhancing the response time for the user. Also, extensive sharing of resources makes CPU switching between threads inexpensive, compared with switching between traditional or heavyweight processes. A register-set switch is still required but no memory-management related work needs to be done.

How?
Two kinds of implementation schemes are in vogue for threads - user-level and kernel-level. User-level threads are managed entirely in user space. The operating system is not aware of their existence. Thread switching does not involve a call to the operating system or an interrupt to the kernel. Switching is thus independent of the operating system and very quick. One disadvantage of this scheme is that if a user-level thread blocks while executing a system call, the kernel blocks the entire process since it is not even aware that other threads exist.

In systems supporting kernel-level threads, the operating system is aware of the existence of multiple threads per process, so when a thread blocks, the operating system chooses the next one to run, either from the same process or a different one.

Some systems support a hybrid approach in which both user-level and kernel-level threads are supported. Solaris 2 is one such system.

Threads in Nachos

In the discussion that follows, we explain all operations on threads in Nachos, focusing on implementation details to enable you to understand what happens inside Nachos when you invoke those operations.

The thread implementation in Nachos provides four basic operations on threads: Fork, Yield, Sleep, and Finish. One implementation detail to remember when using threads is that forking a thread takes two steps. The first involves creating a thread by allocating a data structure for it. Then you can invoke Fork() on the thread object just created.

Creating a thread


                 Thread *t;
                 char id[2], *name = "t";
                 int i, NumThreads = 4;
    
                 for(i=0;i<NumThreads;i++) {
                    sprintf(id,i);
                    strcat(name,id);
                    t = new Thread(name);
                 }

The above code creates 4 threads using the constructor function for the class Thread. The constructor function simply allocates space for thread and sets its status to JUST_CREATED. Note that in Nachos, a "main" thread is created as part of system initialization (see system.cc in code/threads ). Thus, the above code is executed by "main". Note that the ready list remains empty upon creation of these threads.

In Nachos, the global variable currentThread always points to the thread currently occupying the CPU.

Forking a thread


                 Thread *t;
                 char id[2], *name = "t";
                 int i, NumThreads = 4;
    
                 for(i=0;i<NumThreads;i++) {
                    sprintf(id,i);
                    strcat(name,id);
                    t = new Thread(name);
                    t->Fork("somefunction",0);
                 }

The above code completes the job of forking off the threads. Fork() allocates a stack for the thread which invokes it, and adds it to the ready list maintained by the scheduler (see scheduler.cc in code/threads ). "somefunction" is the name of the function that the thread will execute upon occupying the CPU. 0 is the argument to that function. The routine StackAllocate() allocates and initializes the execution stack for the thread. A C routine ThreadRoot() is called which calls the function "somefunction" and upon its return, calls ThreadFinish(). Note that now, we have all four threads in the ready list with their status set to READY. currentThread still points to "main".

Thread Switching

The figure shows the CPU switching between threads t0 and t1. After the switch, t0 is back in the ready list and t1 occupies the CPU. The switch takes place when the routine Run() is invoked. This routine is part of the Scheduler class (see scheduler.cc in the threads directory). Upon invocation, the routine saves the state of the thread currently occupying the CPU and loads the state of the thread being switched to, into the machine registers.

The actual switch takes place by calling the machine dependent context switch routine, SWITCH, defined in switch.s in the threads directory. An important point to note in the Run() routine is what happens after the SWITCH routine returns. All the statements in the Run() routine after the call to SWITCH are executed by the new thread we just switched to! Thus, the thread that invoked the Run() routine is not the same as the one that finishes its execution.

Thread Yield

Yield() is invoked by a thread when it wants to relinquish the CPU, leaving it up to the scheduler to decide which thread to run next. Assume the CPU is occupied by thread t1 and the following code is executed:

               currentThread->Yield();
If the ready list is not empty, the Yield() routine appends this thread to the ready list and assigns the CPU to the thread at the head of the ready list. If there is no other thread on the ready queue, the routine returns without changing the system.

Thus, if the ready list is non-empty, a switch occurs as a result of a thread invoking Yield(). As a result of t1 invoking Yield() , t2 occupies the CPU and t1 returns to the ready list.

Thread Finish and Sleep

Finish() is called when a thread is done executing its forked procedure. As a result, the thread invoking Finish() ceases to exist and the thread at the head of the ready list is assigned the CPU. Thus, a switch takes place here as well.

In Nachos, the global variable threadToBeDestroyed always points to the thread that is to be deallocated. Its default value is NULL and is set by a thread that invokes Finish().

Assume the CPU is occupied by thread t2 and the following code is executed:


               currentThread->Finish();

As a first step, the thread that invokes Finish(), sets the variable threadToBeDestroyed to itself and then invokes Sleep .

The above figure shows the state of the system right before Sleep() is invoked by currentThread. Remember that currentThread is also the thread that invoked Finish() .

The Sleep() routine sets the status of the invoking thread to BLOCKED and invokes Run() to switch to the thread at the front of the ready list. The thread is actually deallocated inside the Run() routine since it can only be deallocated after it has given up the CPU.

Sleep() can be invoked by either a thread that wants to finish or when it is blocked waiting on a synchronization variable. In the first case, the finishing thread sets threadToBeDestroyed causing it to be deallocated by the thread being switched to inside the Run() routine. In the second case, the blocked thread will eventually be woken up by some other thread and put back on the ready list.

Source code in code/threads
Lab
Quiz

Back to Top

Last modified on Friday, 22-Aug-97 23:55:05 EDT