Never call a destructor function directly in your code. It is called
automatically when you call the delete operator.
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.
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.
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.
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".
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.
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.
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
Last modified on Friday, 22-Aug-97 23:55:05 EDT