[OSTEP] Concurrency) 27. Thread API

sunjoo9912·2022년 1월 18일
0

OSTEP 운영체제

목록 보기
15/17
post-thumbnail

OSTEP) 27. Thread API

➖ 22-01-18

Interlude: Thread API

27.1 Thread Creation

  • The first thing you have to be able to do to write a multi-threaded program is to create new threads.

  • Once you create a thread, you really have 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.

  • Thread creation interface in POSIX:

  • There are four arguments: thread, attr, start routine, arg.
  1. thread: a pointer to a structure of type pthread_t;
    -> used to interact with this thread & need to pass it to pthread_create() in order to initialize it.

  2. attr: used to specify any attributes this thread might have (ex. stack size, scheduling priority of the thread, ...)
    -> An attribute is initialized with a separate call to pthread_attr_init();
    -> In most cases, the defaults will be fine (in this case, we will simply pass the value NULL in).

  3. start routine: 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 * , and which returns a value of type void *.
    -> It is just asking: which function should this thread start running in?

  4. arg : the argument to be passed to the function where the thread begins execution.

  • Q. why do we need these void pointers (void *)?
    -> A. 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.

  • (Figure 27.1)

  • we just create a thread that is passed *two arguments, packaged into a single type we define ourselves (myarg_t).

27.2 Thread Completion

  • You need to call the routine pthread _join() to wait for a thread to complete.

  • This routine takes two arguments: thread, value_ptr.
  1. thread: a structure of type pthread_t, 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()).

  2. value_ptr: a pointer to the return value you expect to get back.
    -> the routine 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.

  • (Figure 27.2)

  • 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, myret_t.

1.

  • First, often times we don’t have to do packing and unpacking of arguments.

  • For example, if we just create a thread with no arguments, we can pass NULL
    in as an argument
    when the thread is created.

  • Similarly, we can pass NULL into pthread join() if we don’t care about the return value.

2.

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

  • (Figure 27.3)

  • In this case, we don’t have to package arguments and return values inside of structures.

3.

  • Third, one has to be extremely careful with how values are returned from a thread.

  • Specifically, never return a pointer which refers to something allocated on the thread’s call stack.

  • (Figure 27.2)

  • In this case, the variable oops is allocated on the stack of mythread.
    -> when it returns, the value is automatically deallocated, passing back a pointer to a now deallocated variable will lead to all sorts of bad results.

4.

  • Finally, 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’s called a procedure call.

  • Clearly, we’ll 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.

  • Not 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.

27.3 Locks

  • Beyond thread creation and join, probably the next most useful set of functions provided by the POSIX threads library are those for providing mutual exclusion to a critical section via locks.

  • The most basic pair of routines to use for this purpose is provided by the following:

  • When you have a region of code that is a critical section, and thus needs to be protected to ensure correct operation, locks are quite useful.

  • You can probably imagine what the code looks like:

  • If no other thread holds the lock when pthread mutex_lock() is called:
    -> the thread will acquire the lock and enter the critical section.

  • If another thread hold the lock:
    -> the thread trying to grab the lock will not return from the call until it has acquired the lock (implying that the thread holding the lock has released it via the unlock call).

  • Of course, many threads may be stuck waiting inside the lock acquisition function at a given time; only the thread with the lock acquired, however, should call unlock.

  • Unfortunately, this code is broken, in two important ways.

1.

  • The first problem is a lack of proper initialization.

  • All locks must be properly initialized in order to guarantee that they have the correct values to begin with and thus work as desired when lock and unlock are called.

  1. One way to do this is to use PTHREAD _MUTEX _INITIALIZER.
    -> Doing so sets the lock to the default values and thus makes the lock usable.

  2. The dynamic way to do it (i.e., at run time) is to make a call to pthread_mutex_init().
    -> The first argument is the address of the lock itself, the second is an optional set of attributes (passing NULL in simply uses the defaults).

  • Either way works, but we usually use the dynamic method.

  • Note that a corresponding call to pthread _mutex _destroy() should also be made, when you are done with the lock.

2.

  • The second problem is that it fails to check error codes when calling lock and unlock.

  • If your code doesn’t properly check error codes, the failure will happen silently, which in this case could allow multiple threads into a critical section.

  • Minimally, use wrappers, which assert that the routine succeeded (Figure 27.4).

  • more sophisticated (non-toy) programs, which can’t simply exit when something goes wrong, should check for failure and do something appropriate when a call does not succeed.

  • The lock and unlock routines are not the only routines within the pthreads library to interact with locks.

  • Two other routines of interest:

  • These two calls are used in lock acquisition.

  • The trylock version returns failure if the lock is already held.

  • the timedlock version of acquiring a lock returns after a timeout or after acquiring the lock, whichever happens first.
    -> the timedlock with a timeout of zero degenerates to the trylock case.

  • Both of these versions should generally be avoided.
    -> however, there are a few cases where avoiding getting stuck in a lock acquisition routine can be useful, as we’ll see in future chapters (e.g., deadlock).

➖ 22-01-19

27.4 Condition Variables

  • Condition variables are useful if one thread is waiting for another to do something before it can continue and some kind of signaling must take place between threads.

  • Two primary routines are used:

  • To use a condition variable, one has to have a lock that is associated with this condition.
    -> When calling either of the above routines, this lock should be held.

  • pthread_cond_wait(): puts the calling thread to sleep, and thus waits for some other thread to signal it, usually when something in the program has changed that the now-sleeping thread might care about.

  • In this code, after initialization of the relevant lock and condition, a thread checks to see if the variable ready has yet been set to something other than zero.

  • If not, the thread simply calls the wait routine in order to sleep until some other thread wakes it.

  • The code to wake a thread, which would run in some other thread, looks like this:

1.

  • When signaling (& modifying the global variable ready), we always make sure to have the lock held.
    -> This ensures that we don’t accidentally introduce a race condition into our code.

2.

  • The wait call takes a lock as its second parameter, whereas the signal call only takes a condition.

  • The reason is that the wait call, in addition to putting the calling thread to sleep, releases the lock when putting caller to sleep.

  • Imagine if it did not: how could the other thread acquire the lock and signal it to wake up?

  • However, before returning after being woken, the pthread_cond_wait() re-acquires the lock.
    -> Any time the waiting thread is running between the lock acquire at the beginning of the wait sequence, and the lock release at the end, it holds the lock.

3.

  • the waiting thread re-checks the condition in a while loop, instead of a simple if statement.

  • In general, using a while loop is the simple and safe thing to do.

  • Although it rechecks the condition (perhaps adding a little overhead), there are some pthread implementations that could spuriously wake up a waiting thread.
    -> in such a case, without rechecking, the waiting thread will continue thinking that the condition has changed even though it has not.

  • It is safer to view waking up as a hint that something might have changed, rather than an absolute fact.

  • Sometimes it is tempting to use a simple flag to signal between two threads, instead of a condition variable and associated lock.

  • For example, we could rewrite the waiting code above:

  • we could rewrite the signaling code above:

  • Don’t ever do this, for the following reasons.

  1. It performs poorly in many cases.
    -> spinning for a long time just wastes CPU cycles.

  2. It is error prone.

27.5 Compiling and Running

  • To compile them, you must include the header pthread.h in your code (main.c).

  • On the link line, you must also explicitly link with the pthreads library, by adding the -pthread flag:

27.6 Summary

  • We have introduced the basics of the pthread library, including thread creation, building mutual exclusion via locks, and signaling and waiting via condition variables.

ASIDE: THREAD API GUIDELINES

  • There are a number of small but important things to remember when you use the POSIX thread library (or any thread library) to build a multi-threaded program.

  • Keep it simple.
    -> Any code to lock or signal between threads should be as simple as possible.

  • Minimize thread interactions.

  • Initialize locks and condition variables.

  • Check your return codes.

  • Be careful with how you pass arguments to, and return values from, threads.
    -> In particular, any time you are passing a reference to a variable allocated on the stack, you are probably doing something wrong.

Each thread has its own stack.
-> if you have a locally-allocated variable inside of some function a thread is executing, it is essentially private to that thread.
-> To share data between threads, the values must be in the heap or otherwise some locale that is globally accessible.

Always use condition variables to signal between threads.
-> While it is often tempting to use a simple flag, don’t do it.

Use the manual pages.

📌참고자료

profile
✧°₊·♡∗ˈ‧₊

0개의 댓글