You are expected to understand this. CS 111 Operating Systems Principles, Spring 2006
You are here: CS111: [[2006spring:notes:lec5]]
 
 
 

Threads

A Thread is:

  • a process without isolation
  • property of an abstract machine
  • an execution context, where there are one or more execution contexts per process (multiprocessor abstract machine).

When two virtual processors are inhabiting the same address, threads make the machine look like a multiprocessor instead of a uniprocessor.

Process Descriptor

When a process or thread is forked, the new forked process or thread has both similar and different process descriptor properties from the original process and thread. These similarities and differences are shown in the table below. Depending on what part of the process descriptor is being forked, the new forked process or thread has either a new copy of the original, a shared copy of the original, or a brand new version different from the original.

.:process-descriptor-chart.jpg

Review:

Q: What are the three parts of the process descriptor? 
A:   - Accounting
     - Abstract Machine
     - Kernel Resources and Kernel State

Thread Descriptor : the kernel’s view of a thread (execution context). Each thread has a separate thread ID.

      Q. What type of information is contained in the thread descriptor? 
      A. Statistics, registers, eip, esp, State, Kernel stack, wait queues 
         (All the entities that are indicated as copied or new in the above table)

In processes, everything is copied. With threads, as little is copied as possible. Copy- the new thread and the old thread have copies of the same thing, with each copy being independent of one another. Shared - the new thread and the old thread have access to the same thing, and and therefore dependent on each other.

Kernel stack – area reserved for responding to a system call. The kernel stack is where intermediate results can be stored. Wait Queue - If a process/thread makes a blocking system call, then that process/thread is going to go into a wait queue.

      Q. Which of these objects are part of the execution context, and which is not?
      A. Those that are copied - kernal stack, wait queue, etc...(refer to above table)

What does a system call look like? It is part of the execution context. Thus, they are not shared but are copied.

Accounting – The process ID is part of the Accounting Category.

What happens to the process ID when we fork a new process/thread?

  • New process ID for a new process.
  • Same process ID for a new thread.

Process Statistics- New for a new process/thread.

      Q. What happens when you fork a process that has multiple threads?
      A. Only the current thread is copied.

Thread Interfaces

Creating a thread

Idea: Can we make a version of fork() for threads?? This is a BAD IDEA in fact, as we can see by working through a hypothetical example. We propose a hypothetical system call called tfork(), for Thread Fork, which creates a new thread.

tfork() returns 0 in the child thread, and the child's thread ID in the parent thread.

Process creation: fork () or tfork()

  1. 0 in child
  2. child id in parent

Sample code:

 
	int main (int c, char** v) {
	int x;
	if (tfork ( ) ==0) {
		x = 2;
	} else {
		x = 3;
	}
	printf(“%d”, x);

With a regular fork, this code would print 32 or 23, since we aren't sure if the child or parent will run first. We would want tfork to do the same and print out 32 or 23, but it could possibly print out 33, 22, 32, or 23. What’s going on here? How does it print out 22 or 33?

PROBLEM

The stack is part of the execution context and allows us to have recursively called functions. Stacks are stored in the address space, but the address space in threads is always shared. Thus, we have two execution contexts that are touching the same variable on the stack. They have different registers, but they share the stack. Assuming x is stored on the stack, it is shared between the child and parent, since the stack is a shared address space. Therefore, the output is unpredictable. For example, the child thread sets x = 2, then goes to sleep, the parent then can set x = 3, consequently also changing the value of x in the child to 3. So in the end, our interface for creating a new thread was a BAD IDEA!

RECAP: Since stacks are part of the execution context in normal programming languages, when we create a new thread, we should also create a new stack.

Note: When you create a new process, it starts out with one new thread.

POSSIBLE SOLUTIONS

  1. Find some address space for a new stack (the kernel only needs to be involved, if you do not have enough space already).
  2. Create a new thread running on that stack (the new thread will have a different esp).
  3. New thread has an EMPTY stack
  4. New function that runs in thread

Below, is an example of Solution #2:

     int main (int c, char **v) {
	int x;
	int *xp = &x;
	if (tfork() == 0)
            *xp=2;
        else
            *xp=3;
        printf("%d",*xp);
Posix Threads (standard thread implementation)
int pthread_create (pthread_t * new_thread_id, pthread_attr_t *attr, void (*start)(void *), void *start_argument))

We supply a new function that is going to be called. We don’t continue running on the same function, but instead start on a new one. 1-Returns new threads id, 2-set of thread attributes (not talked about in class) 3-function that gets called on the new stack

void pthread_exit //exits a thread
int pthread_join //equivalent of wait

It will be faster to fork threads vs processes, since the address spaces don't need to be copied.

MP3 player Version 1 (abstracted)

char buf [2048];
int main (int c, char "**x") 
{
	while (!feof(stdin))
        {
		read_frame(&buf[0]);
		process_audio (&buf[0]);
		process_video (&buf[0]); 
	}
}
//Problem: The audio will first be processed, and then the video will be processed. Thus, there is no synchronization.

MP3 player Version 2 (abstracted)

char buf [2048];
int main (int c, char **x) 
{
	while (!feof(stdin))
        {
		read_frame(&buf[0]);
                if(fork( )==0)
                      process_audio (&buf[0]);
		process_video (&buf[0]); 
	}
}
//Problem: Now, the problem is that the other thread was not exited.

MP3 player Version 3 (abstracted)

char buf [2048];
int main (int c, char **x) 
{
	while (!feof(stdin))
        {
		read_frame(&buf[0]);
                if(fork( )==0) 
                {
                      process_audio (&buf[0]); 
                      exit (0);
                }
		process_video (&buf[0]); 
	}
}

MP3 player Version 4 (abstracted)

char buf [2048];
int main (int c, char **x) 
{
	while (!feof(stdin))
        {	
               pthread_t audio;
               read_frame(&buf[0]);
               if(fork( )==0) 
               {
                     process_audio (&buf[0]); 
                     exit (0);
               }
	       pthread_create (&audio, NULL, &process_audio, &buf[0]); 
              
              // The process_audio thread will exit when it’s done processing the frame. 
              // But when does the audio thread complete?
                
              Process_video (&buf[0]); 
	}
}
//Problem: What happens if the video finishes processing before the audio finishes processing? 

The changes the parent process makes to the buffer, are now visible to the child. Before with processes, you didn’t need to worry about buffers being changed underneath. However, now with threads, you do.

MP3 player Version 5 (abstracted)

char buf [2048];
int main (int c, char **x) 
{
	while (!feof(stdin))
        {	
                pthread_t audio;
		read_frame(&buf[0]);
                if(fork( )==0) 
                {
                      Process_audio (&buf[0]); 
                      exit (0);
                }
		pthread_create (&audio, NULL, &process_audio, &buf[0]); 
                process_video (&buf[0]); 
                pthread_join(audio, &status); //equivalent of waitpid
	}
}
//Problem: This still has a lot of overhead. We want only want to create 
one thread to process all the audio, and one thread to process all the video

MP3 player Version 6 (abstracted)

char buf [2048];
int main (int c, char **x) 
{
	pthread_t audio;		
        pthread_create (&audio, NULL, &process_audio, &buf[0]); 
	while (     ){	
		Read_frame(&buf[0]);
                Process_video (&buf[0]); 
	}
}

Now, we don’t have pthread_join because we don’t want the audio thread to exit until all the frames have been processed. Instead, we want to do some kind of waiting (without exiting the thread). This sounds like something along the lines of what sleep does.

MP3 player Version 7 (abstracted)

char buf [2048];
int main (int c, char **x) {
	pthread_t audio;		
        pthread_create (&audio, NULL, &process_audio, &buf[0]); 
	while (     ){	
		read_frame(&buf[0]);
		frame_ready=1;
                process_video (&buf[0]);
                while(!audio_done) /* do nothing */;
                audio_done=0; //reset value; 
	}
}

void process_audio (void *buf() ){
	while(1) {
		while(!frame_ready)/* */;   ///busy waiting
		frame_ready=0;
		/*play audio*/
		audio_done=1;
	}
}
//PROBLEM: Busy waiting.

Signals

Example of a signal:

int sleep (int time) //blocks process for “time” seconds
  • Synchronous : notified of event during system call
  • Asynchronous : notified of event even when you’re not making a system call (even if we’re in an infinite loop)
int pthread_kill (pthread_t pt, int signo)	//delivers this signal to this thread
	Signo: names this event. 321 event types: kill, segv, usr1
void signal (int singo, void (*sighandler)(int));
	Supplies a handler for a signal
when signal signo is delivered,
	Call the “sighandler” function

Generally, sighandlers generally just set a few flags; may make some system calls. CAUTION: Many operations are not safe for sighandlers; e.g. malloc,

Now that we know about signals, we can get rid of all busy waiting).

MP3 player Version 8 (abstracted)

char buf [2048];
int main (int c, char **x) {
	pthread_t audio;		
        pthread_create (&audio, NULL, &process_audio, &buf[0]); 
        
        while (     ){	
		read_frame(&buf[0]);
		frame_ready=1; Pthread_kill(audio, SIGUSR1);
                process_video (&buf[0]);
                while(!audio_done) /**/; 
                audio_done=0; //reset value; 
                //Implement sleep here
	}
}

void process_audio (void *buf() ){
	while(1) 
        {
		void handler (int) { }
		Signal (SIGUSR1, &handler);
                while(!frame_ready)
                     /* sleep(infinite time)*/;   ///busy waiting
		frame_ready=0;
		/*play audio*/
		audio_done=1;
		//Sends a signal here. 
	}
}


What happens when video calls audio? It will interrupt audio’s sleep.

Kernel Threads vs. User Threads
  • Kernel Threads: T kernel has a thread descriptor for each thread.
  • User Threads: The application emulates multiple thread descriptors in one execution context.
  • User level code can allocate a stack, store a copy of registers, make a new user level thread descriptor, can switch between threads by saving and loading registers to memory. Users can only willingly switch from one thread to another, which requires cooperation on both ends.
  • User threads = cooperative multitasking. Cooperative multitasking implies that a new task is run only when the current task is willing to switch.

Which of these things, needs special kernel support that is specific to threads?

  1. Finding some address space for a new stack? YES, the kernel only needs to be involved, if there is not enough space already.
  2. Creating a new thread running on that stack? YES, because the new thread has a different esp.
  3. New thread has an EMPTY stack. YES
  4. New function that runs in thread. YES
 
2006spring/notes/lec5.txt · Last modified: 2006/09/26 11:42 (external edit)
 
Recent changes RSS feed Driven by DokuWiki