Chapter: Network Programming and Management : Advanced Sockets

Threads

When a process needs something to be performed by another entity, it forks a child process and lets the child perform processing.

THREADS

 

When a process needs something to be performed by another entity, it forks a child process and lets the child perform processing. (similar to concurrent server program.) The problem associated with this are:

a. All descriptors are copied from parent to child thereby occupying more memory.

b. Inter process communication requires to pass information between the parent and child after each fork. Returning the information form the child to parent takes more work.

 

Threads being a light weight process help to overcome these drawbacks. However, as they share the same variables – known as global variables - they need to be synchronized to avoid errors. IN addition to these variables, threads also share

 

• Process instructions

 

• Data,

 

• Open files

 

• Signal handlers and signal dispositions

 

• Current working directory

 

• User Groups Ids.

 

Each thread has its own thread ID, set of registers, program counter and stack pointer, stack, errno, signal mask and priority.

 

Basic thread functions: pthread_create function:

 

int ptheread_create (pthread_t) *tid, const pthread_attr_t *attr, void * (*func)

(*void), void *arg );

tid : is the thread ID whose data type is pthread_t - unsigned integer. ON successful creation of thread, its ID is returned through the pointer tid.

 

pthread_atttr_t : Each thread has a number of attributes – priority, initi8al stack size, whether is demon thread or not. If this variable is specified, it overrides the default. To accept the default, attr argument is set to null pointer.

 

*func : When the thread is created, a function is specified for it to execute. The thread starts by calling this function and then terminates either explicitly (by calling pthread_exit) or implicitly by letting this function to return. The address of the function is specified as the func argument. And this function is called with a single

 

pointer argument, arg. If multiple arguments are to be passed, the address of the structure can be passed

The function takes one argument – a generic pointer (void *) and returns a generic pointer ( void *). This lets us to pass one pointer to the thread and return one pointer.

 

The return is normally 0 if OK or nonzero on an error

 

 

pthread_join function :

 

int pthread_join (pthread_t tid, void ** status )

We can wait for a given                                                        thread to terminate by calling pthread_join .                                                            We  must

 

specify the tid of the thread that we wish to wait for. If the status pointer is non null, the return value from the thread is stored in the location pointed to by status.

 

pthread_self function : Each thread has an ID that identifies it within a given process. The thread ID is returned by pthread_create. This function fetches this value for itself by using this function:

 

pthread_t pthread_self(void);

 

pthread_detach function :

 

A thread is joinable (the default) or detached. When a joinable thread terminates, its thread ID and exit status are retained until thread calls pthread_join(). But a detached thread for example daemon thread- when it terminates all its resources are released and we cannot wait for it terminate.

 

When one thread needs to know when another thread terminates, it is best to leave the thread joinable.

 

Int pthread_detach (pthread_t tid);

 

pthread_exit function:

 

One way for the thread to terminate is to call pthread_exit(). 

void pthread_exit (void *status);

 

The str_cli function uses fputs and the fgets to write to and read from the server. While doing so, fgets my be putting the data from stdin into the buffer wherein already there is data waiting to be writen. This will block the  other processes  as the function is waiting to write the pending data. If  there is data to be read form the server by the function readen at the client, it will be blocked.  Similarly, if a line of input is available from the socket  we can block in the subsequent call to fputs, if the standard  output is slower thant the network. IN such situation non blocking methods are used. This requires creating elaborate  arrangement of buffer management: where pointer are used to find the  data that is already sent, data that are yet to be sent and available space in the read buffer. As shown below:


The program        comes to be about 100 lines .  This can be reduced       by      using threads.          In  this, call to pthread is given in the main function     where  in     the              fgets function are invoked.  When the thread returns, fputs is invoked.                               

 

Mutexes: Mutual Exclusion

 

Notice the following Figure1.a that when a thread terminates, the main loop decrements both nconn and nlefttoread. We could have placed these two decrements in the function do_get_read, letting each thread decrement these two counters immediately before the thread terminates. But this would be a subtle, yet significant, concurrent programming error.

 

 

threads/web01.c

 

while (nlefttoread > 0) {

 

while (nconn < maxnconn && nlefttoconn > 0) {

/* find a file to read */

for (i = 0; i < nfiles; i++)

if (file[i].f_flags == 0)

break;

if (i == nfiles)

err_quit("nlefttoconn = %d but nothing found", nlefttoconn);

file[i].f_flags = F_CONNECTING;

Pthread_create(&tid, NULL, &do_get_read, &file[i]);

file[i].f_tid = tid;

nconn++;

nlefttoconn--;

}

if ( (n = thr_join(0, &tid, (void **) &fptr)) != 0)

errno = n, err_sys("thr_join error");

nconn--;

nlefttoread--;

printf("thread id %d for %s done\n", tid, fptr->f_name);

}

exit(0);

}

 

The problem with placing the code in the function that each thread executes is that these two variables are global, not thread-specific. If one thread is in the middle of decrementing a variable, that thread is suspended, and if another thread executes and decrements the same variable, an error can result. For example, assume that the C compiler turns the decrement operator into three instructions: load from memory into a register, decrement the register, and store from the register into memory. Consider the following possible scenario:

 

Thread A is running and it loads the value of nconn (3) into a register.

 

 

The system switches threads from A to B. A's registers are saved, and B's registers are restored.

 

Thread B executes the three instructions corresponding to the C expression nconn--,

 

Sometime later, the system switches threads from B to A. A's registers are restored and A continues where it left off, at the second machine instruction in the three-instructionsequence. The value of the register is decremented from 3 to 2, and the value of 2 is stored in nconn.

 

The end result is that nconn is 2 when it should be 1. This is wrong.

 

These types of concurrent programming errors are hard to find for numerous reasons. First, they occur rarely. Nevertheless, it is an error and it will fail (Murphy's Law). Second, the error is hard to duplicate since it depends on the nondeterministic timing of many events. Lastly, on some systems,the hardware instructions might be atomic; that is, there might be a hardware instruction to decrement an integer in memory (instead of the three-instruction sequence we assumed above) and the hardware cannot be interrupted during this instruction. But, this is not guaranteed by all systems, so the code works on one system but not on another.

 

We call threads programming concurrent programming, or parallel programming, since multiple threads can be running concurrently (in parallel), accessing the same variables. While the error scenario we just discussed assumes a single-CPU system, the potential for error also exists if threads A and B are running at the same time on different CPUs on a multiprocessor system. With normal Unix programming, we do not encounter these concurrent programming problems because with fork, nothing besides descriptors is shared between the parent and child. We will, however, encounter this same type of problem when we discuss shared memory between processes.

 

 

We can easily demonstrate this problem with threads. Figure 2 is a simple program that creates two threads and then has each thread increment a global variable 5,000 times.

 

We exacerbate the potential for a problem by fetching the current value of counter, printing the new value, and then storing the new value. If we run this program, we have the output shown in Figure 1.b.

Figure 2 Two threads that increment a global variable incorrectly.

 

 

threads/example01.c

 

 

1 #include  "unpthread.h"

 

2 #define NLOOP 5000

 

3 int  counter;          /* incremented by threads */

 

4 void        *doit(void *);

 

5 int

 

6 main(int argc, char **argv)

7  {

8         pthread_t tidA, tidB;

 

9   Pthread_create(&tidA, NULL, &doit, NULL);

 

10        Pthread_create(&tidB, NULL, &doit, NULL);

 

11                /* wait for both threads to terminate */

12        Pthread_join(tidA, NULL);

13        Pthread_join(tidB, NULL);

 

14        exit(0);

15 }

 

16 void *

 

17 doit(void *vptr)

18 {

19   int  i, val;

 

20          /*

21            * Each thread fetches, prints, and increments the counter NLOOP times.

22            * The value of the counter should increase monotonically.

23            */

 

24          for (i = 0; i < NLOOP; i++) {

25                  val = counter;

26                  printf("%d: %d\n", pthread_self(), val + 1);

27                  counter = val + 1;

28          }

 

29          return (NULL);

}

 

Notice the error the first time the system switches from thread 4 to thread 5: The value 518 i sstored by each thread. This happens numerous times through the 10,000 lines of output.

 

The nondeterministic nature of this type of problem is also evident if we run the program a few times: Each time, the end result is different from the previous run of the program. Also, if we redirect the output to a disk file, sometimes the error does not occur since the program runs faster, providing fewer opportunities to switch between the threads. The greatest number of errors occurs when we run the program interactively, writing the output to the (slow) terminal, but saving the output in a file using the Unix script program (discussed in detail in Chapter 19 of APUE).

 

The problem we just discussed, multiple threads updating a shared variable, is the simplest problem. The solution is to protect the shared variable with a mutex (which stands for "mutual exclusion") and access the variable only when we hold the mutex. In terms of Pthreads, a mutex is a variable of type pthread_mutex_t. We lock and unlock a mutex using the following two functions:

 

#include <pthread.h>

int     pthread_mutex_lock(pthread_mutex_t      * mptr);

int     pthread_mutex_unlock(pthread_mutex_t  * mptr);

Both return: 0 if OK, positive Exxx           value on error

If we try to lock a mutex that is already locked by some other thread, we are blocked until the mutex is unlocked.

 

If a mutex variable is statically allocated, we must initialize it to the constant PTHREAD_MUTEX_INITIALIZER. We will see in next Section that if we allocate a mutex in shared memory, we must initialize it at runtime by calling the pthread_mutex_init function.

 

Some systems (e.g., Solaris) define PTHREAD_MUTEX_INITIALIZER to be 0, so omitting this initialization is acceptable, since statically allocated variables are automatically initialized to 0. But there is no guarantee that this is acceptable and other systems (e.g., Digital Unix) define the initializer to be nonzero.

 

 

Figure3 is a corrected version of Figure 2 that uses a single mutex to lock the counter between the two threads.

 

Figure 3 Corrected version of Figure 2 using a mutex to protect the shared variable.

 

threads/example02.c

1 #include   "unpthread.h"

 

2 #define NLOOP 5000

3 int  counter;          /* incremented by threads */

4 pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

 

5 void  *doit(void *);

6 int

7 main(int argc, char **argv)

8  {

9         pthread_t tidA, tidB;

 

10        Pthread_create(&tidA, NULL, &doit, NULL);

11        Pthread_create(&tidB, NULL, &doit, NULL);

 

12                /* wait for both threads to terminate */

13        Pthread_join(tidA, NULL);

14        Pthread_join(tidB, NULL);

 

15        exit(0);

16 }

17 void *

18 doit(void *vptr)

19 {

20   int   i, val;

 

21        /*

22          * Each thread fetches, prints, and increments the counter NLOOP times.

23          * The value of the counter should increase monotonically.

24          */

 

25        for (i = 0; i < NLOOP; i++) {

26                Pthread_mutex_lock(&counter_mutex);

 

27                val = counter;

28                printf("%d: %d\n", pthread_self(), val + 1);

29                counter = val + 1;

 

 

30                Pthread_mutex_unlock(&counter_mutex);

31        }

 

32        return (NULL);

33 }

 

We declare a mutex named counter_mutex and this mutex must be locked by the thread before the thread manipulates the counter variable. When we run this program, the output is always correct: The value is incremented monotonically and the final value printed is always 10,000.

 

How much overhead is involved with mutex locking? The programs in Figures 2 and 3 were changed to loop 50,000 times and were timed while the output was directed to /dev/null.The difference in CPU time from the incorrect version with no mutex to the correct version that used a mutex was 10%. This tells us that mutex locking is not a large overhead


Study Material, Lecturing Notes, Assignment, Reference, Wiki description explanation, brief detail
Network Programming and Management : Advanced Sockets : Threads |


Privacy Policy, Terms and Conditions, DMCA Policy and Compliant

Copyright © 2018-2024 BrainKart.com; All Rights Reserved. Developed by Therithal info, Chennai.