Книга: Programming with POSIX® Threads

8.1.1 Avoid relying on "thread inertia"

8.1.1 Avoid relying on "thread inertia"

Always, always, remember that threads are asynchronous. That's especially important to keep in mind when you develop code on uniprocessor systems where threads may be "slightly synchronous." Nothing happens simultaneously on a uniprocessor, where ready threads are serially timesliced at relatively predictable intervals. When you create a new thread on a uniprocessor or unblock a thread waiting for a mutex or condition variable, it cannot run immediately unless it has a higher priority than the creator or waker.

The same phenomenon may occur even on a multiprocessor, if you have reached the "concurrency limit" of the process, for example, when you have more ready threads than there are processors. The creator, or the thread waking another thread, given equal priority, will continue running until it blocks or until the next timeslice (which may be many nanoseconds away).

This means that the thread that currently has a processor has an advantage. It tends to remain in motion, exhibiting behavior vaguely akin to physical inertia. As a result, you may get away with errors that will cause your code to break in mysterious ways when the newly created or awakened thread is able to run immediately — when there are free processors. The following program, inertia.c, demonstrates how this phenomenon can disrupt your program.

27-41 The question is one of whether the thread function printer_thread will see the value of stringPtr that was set before the call to pthread_create, or the value set after the call to pthread_create. The desired value is "After value." This is a very common class of programming error. Of course, in most cases the problem is less obvious than in this simple example. Often, the variable is uninitialized, not set to some benign value, and the result may be data corruption or a segmentation fault.

39 Now, notice the delay loop. Even on a multiprocessor, this program won't break all the time. The program will usually be able to change stringPtr before the new thread can begin executing — it takes time for a newly created thread to get into your code, after all, and the "window of opportunity" in this particular program is only a few instructions. The loop allows me to demonstrate the problem by delaying the main thread long enough to give the printer thread time to start. If you make this loop long enough, you will see the problem even on a uniprocessor, if main is eventually timesliced.

? inertia.c

1 #include <pthread.h>
2 #include "errors.h"
3
4 void *printer_thread (void *arg)
5 {
6 char *string = *(char**)arg;
7
8 printf ("%sn", string);
9 return NULL;

10 }
11
12 int main (int argc, char *argv[])
13 {
14 pthread_t printer_id;
15 char *string_ptr;
16 int i, status; 17
18 #ifdef sun
19 /*
20 * On Solaris 2.5, threads are not timesliced. To ensure
21 * that our two threads can run concurrently, we need to
22 * increase the concurrency level to 2.
23 */
24 DPRINTF (("Setting concurrency level to 2n"));
25 thr_setconcurrency (2);
26 #endif
27 string_ptr = "Before value";
28 status = pthread_create (
29  &printer_id, NULL, printer_thread, (void*)&string_ptr);
30 if (status != 0)
31  err_abort (status, "Create thread");

32
33 /*
34 * Give the thread a chance to get started if it's going to run
35 * in parallel, but not enough that the current thread is likely
36 * to be timesliced. (This is a tricky balance, and the loop may
37 * need to be adjusted on your system before you can see the bug.)
38 */
39 for (i = 0; i < 10000000; i++);

40
41 string_ptr = "After value";
42 status = pthread_join (printer_id, NULL);
43 if (status != 0)
44  err_abort (status, "Join thread");
45 return 0;
46 }

The way to fix inertia.c is to set the "After value," the one you want the threads to see, before creating the thread. That's not so hard, is it? There may still be a "Before value," whether it is uninitialized storage or a value that was previously used for some other purpose, but the thread you create can't see it. By the memory visibility rules given in Section 3.4, the new thread sees all memory writes that occurred prior to the call into pthread_create. Always design your code so that threads aren't started until after all the resources they need have been created and initialized exactly the way you want the thread to see them.

Never assume that a thread you create will wait for you.

You can cause yourself as many problems by assuming a thread will run "soon" as by assuming it won't run "too soon." Creating a thread that relies on "temporary storage" in the creator thread is almost always a bad idea. I have seen code that creates a series of threads, passing a pointer to the same local structure to each, changing the structure member values each time. The problem is that you can't assume threads will start in any specific order. All of those threads may start after your last creation call, in which case they all get the last value of the data. Or the threads might start a little bit out of order, so that the first and second thread get the same data, but the others get what you intended them to get.

Thread inertia is a special case of thread races. Although thread races are covered much more extensively in Section 8.1.2, thread inertia is a subtle effect, and many people do not recognize it as a race. So test your code thoroughly on a multiprocessor, if at all possible. Do this as early as possible during development, and continuously throughout development. Do this despite the fact that, especially without a perfect threaded debugger, testing on a multiprocessor will be more difficult than debugging on a uniprocessor. And, of course, you should carefully read the following section.

Оглавление книги


Генерация: 1.115. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз