Operating System: Three Easy Pieces --- Thread API (Note)

This chapter briefly covers the main properties of the thread API. Each part will be explained

further in the subsequent chapters, as we know how to use the API. More details can be found

in various books and online sources. We should note that the subsequent chapters introduces

the concepts of locks and condition variables more slowly, with many examples; this chpater is

thus better used as a reference.

              Crux: How to Create and Control Threads

What interface should the OS present for thread creation and control ? How should these 

interfaces be designed to enable ease use of as well as utility?

                   Thread Creation

The first thing you have to be aboe to write a multi-threaded program is to create new threads,

and thus some kind of thread creation interface must exist. In POSIX, it is easy:

#include <pthread.h>
int pthread_create(pthread_t* thread, const pthread_attr_t* attr.
                            void* (*start_routine)(void*), void* arg);                            

This declaration might look a little complex (particularly if you haven't used function pointers in

C), but actually it's not too bad. There are four arguments: thread, attr, start_routines, and arg.

The first, thread, is a pointer to a structure of type pthread_t; we will use this structure to 

interact with this thread, and thus we need to pass it to pthread_create() in order to initialize it.

The second argument, attr, is used to specify any attributes this thread might have. Some

example include setting the stack size or perhaps information about the scheduling priority of

the thread. An attribute is initialized with a separate call to pthread_attr_init(); see the mannual

page for details. However, in most cases, the defaults will be fine; in this case, we will simply 

pass the value NULL in.

The third argument is the most complex, but is really just asking: which function should this

thread start running in? In C, we call this a function pointer, and this one tells us the following

is expected: a function name (start_routine), which is passed a single argument of type void*

(as indicated in the parenthese after start_routine), and which returns a valur of type void*.

If this routine instead required an integer argument, instead of a void pointer, the declaration

would look like this:

int pthread_create(...,
                           void* (*start_routine)(int),
                           int arg);
                               

If instead the routine took a void pointer as an argument, but returned an integer, it would look

look this:

int pthread_create(...,
                            int (*start_routine)(void*),
                            void* arg);

Finally, the fouth argument, arg, is exactly the argument to be passed to the function where

the thread begins execution. You might ask: why do we need these void pointers? Well, the

answer is quite simple: having a void pointer as an argument to the function start_routine allows

us to pass in any type of argument; having it as a return value allows the thread to return any

type of result.

Let's look at an example. Here we just create a thread that is passed two arguments, packaged

into a single type we define ourselves. The thread, once created, can simply cast its argument to

the type it expects and thus unpack the arguments as desired.

And there it is! Once you create a thread, you really ahve another live executing entity, complete

with its own call stack, running within the same address space as all the currently existing 

threads in the program. The fun thus begins!

#include <pthread.h>

typedef struct __myarg_t {
      int a;
      int b;
} myarg_t;

void* mythread(void* arg) {
      myarg_t* m = (myarg_t*)arg;
      printf("%d %d
", m->a, m->b);
 
      return NULL;
}

int main(int argc, char* argv[]) {
     
      pthread_t p;
      int rc;
 
      myarg_t args;
      args.a = 10;
      args.b = 20;
      rc = pthread_create(&p, NULL, mythread, &arg);
      ......
}

                    Thread Completion

The example above shows how to create a thread. However, what happens if you want to wait

for a thread to complete? You need to do something special in order to wait for completion;

in particular, you must call the routine pthread_join(). 

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

This routine takses two arguments. The first is of type pthread_t, and is used to specify which

thread to wait for. This variable is initialized by the thread creation routine (when you pass a

pointer to it as an argument to pthread_create()); if you keep it around, you can use it to wait

for that thread to terminate.

The second argument is pointer to the return value you expect to get back. Because the routine

can return anything, it is defined to return a pointer to void; because the pthread_join() routine

changes the value of the passed in argument, you need to pass in a pointer to that value, not

just the value itself.

Let's look at another example. In the code, a single thread is again created, and passed a couple

of arguments via the myarg_t structure. To return values, the myret_t type is used. Once the

thread is finished running, the main thread, which has been waiting inside of the pthread_join()

routine, then returns, and we can access the values returned from the thread, namely is in

myarg_t.

Second, if we are just passing in a single value (e.g., an int), we don't have to package it up

as an argument. Figure 27.3 shows an example. In this case, life is a bit simpler, as we don not

have to package arguments and return values inside of functions.

Third, we should note that one has to be extremely careful with how values are returned from

a thread. In particular, never return a pointer which refers to something allocated on the thread's

call stack. If you do, what do you think will happen? (think about it!) Here is an example of a 

dangerous piece of code, modified from the exampe in Figure 27.2.

void* mythread(void* arg) {
     myarg_t*  m = (myarg_t*)arg;
     printf("%d %d
", m->a, m->b);
     myret_t r;
     r.x = 1;
     r.y = 2;
     
     return (void*)&r;
}

In this case, the variable r is allocated on the stack of mythread. However, when it returns, the

value is automatically deallocated (that's why the stack is so easy to use, after all!), and thus

passing back a pointer to a now deallocated variable will lead to all sorts of bad results. 

Certainly, when you print out the values you think you returned, you will probably (but not

necessarily!) be surprised. Try it and find out for yourself.

Finally, you might notice that the use of pthread_create() to create a thread, followed by an 

immediate call to pthread_join(), is a pretty strange way to create a thread. In fact, there is an

easier way to accomplish this exact task; it is called a procedure call. Clearly, we will usually be

creating more than just one thread and waiting for it to complete, otherwise there is not much

purpose to using threads at all.

We should note that all code that is multi-threaded uses the join routine. For example, a 

multi-threaded web server might create a number of worker threads, and then use the main 

thread to accept requests and pass them to the workers, indefinitely. Such long-lived programs

thus may not need to join. However, a parallel program that creates threads to execute a

particular task in parallel will likely use join to make sure all such work completes before exiting

or moving onto the next stage of computation.

原文地址:https://www.cnblogs.com/miaoyong/p/4966134.html