Home arrow Site Administration arrow Page 5 - Design and Architecture

Multithreading - Administration

Servers typically need to be able to handle multiple clients simultaneously. This presents several problems that need to be solved. This article addresses three of those issues: allowing multiple clients to connect and stay connnected, efficient use of resources, and keeping the server responsive to each of the clients. It is excerpted from chapter five of the book The Definitive Guide to Linux Networking Programming, written by Keir Davis et. al. (Apress, 2004; ISBN: 1590593227).

TABLE OF CONTENTS:
  1. Design and Architecture
  2. Multiplexing
  3. Forking
  4. Preforking: Process Pools
  5. Multithreading
  6. Combining Preforking and Prethreading
  7. Dealing with Large Amounts of Data
By: Apress Publishing
Rating: starstarstarstarstar / 22
November 03, 2005

print this article
SEARCH DEV SHED

TOOLS YOU CAN USE

advertisement

More recently, using threads has become the preferred method for handling multiple clients. Threads are lightweight processes that share the main memory space of the parent process. Because of this, they use fewer resources than a multiprocess application, and they enjoy a faster context-switch time. However, multithreaded applications are not as stable as multiprocess applications. Because of the shared memory if, say, a buffer overrun occurs in one thread, it can impact other threads. In this way, one bad thread can bring down the entire server program. This isnít the case with multiprocess applications, where the memory in each process is protected from alteration from another process by the operating system. This keeps an errant process from corrupting the memory of another process.

Before moving on, letís talk a little more about shared memory in multi-threaded server applications. If not handled correctly, the shared memory can be a double-edged sword. Remember that global variables will be shared by all threads. This means that to keep client-specific information, you must take advantage of the thread-local storage mechanisms provided by your thread library. These allow you to create ďthread-globalĒ values that arenít shared between threads.

If you do have global variables that need to be shared between threads, it is very important to use the synchronization objects like mutexes to control access to them. Without synchronization objects, you can run into very strange behaviors and even unexplained program crashes. Most often this occurs when one thread is writing to a variable when another is reading or writing to the same variable. This situation can cause memory corruption, and it may not show itself immediately but will eventually cause problems. Multithreaded applications are hard enough to debug, so synchronize access to all global variables and structures.

The version of POSIX threads distributed with most flavors of Linux was developed by Xavier Leroy. His website,http://pauillac.inria.fr/~xleroy/  linuxthreads, has more information. In addition, there are many other resources on the Internet for references on pthread programming. You can find a new thread library based on GNUís pth library athttp://oss.software.ibm.com/ developerworks/opensource/pthreads.

As with the multiprocess strategy, using one thread per client is the simplest multithreaded server architecture. Again, the client logic in each thread does not have to stop to service other connected clients, but is free to focus on one client. One caveat, though, is that the maximum number of threads allowed on a system is far less than the maximum number of processes. With a server that needs to maintain a large number of persistent connections, you may want to consider using one of the other architectures presented in this chapter.

A Multithreaded Server

Note the multithreaded serverís similarity to the multiprocess model. In the following program (server4.c), the parent server process waits for client connections. When a connection occurs, the server creates a new thread and passes the new socket descriptor to it. The new thread then reads data from the client and echoes it back. Finally, the connection is closed, and the thread exits. Meanwhile, the parent process loops and waits for another connection. Figure 5-7 shows the basic architecture for a multithreaded server.


Figure 5-7.  Basic architecture for a multithreaded server

Again, the initial section of the code is similar to that in the previous programs:

/* server4.c */
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
void* thread_proc(void *arg);
int main(int argc, char *argv[])
{

First, we declare the variables that we will need.

struct sockaddr_in sAddr;
int listensock;
int newsock;
int result;
pthread_t thread_id;
int val;

Next, we create the socket that will listen for incoming connections.

  listensock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

Now we set theSO_REUSEADDRsocket option.

  val = 1;
  result = setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
  if (result < 0) {
         
perror("server4");
          return 0;
  }

Then, we bind it to a local port and all addresses associated with the machine.

  sAddr.sin_family = AF_INET;
  sAddr.sin_port = htons(1972);  
  sAddr.sin_addr.s_addr = INADDR_ANY;
 
result = bind(listensock, (struct sockaddr *) &sAddr, sizeof(sAddr));
 
if (result < 0) {
    perror("server4");
    return 0;
 
}

We then put the socket into listening mode.

  result = listen(listensock, 5);
 
if (result < 0) {
    perror("server4");
    return 0;
 
}

As inserver2.c, we callaccept()and let it block until a client tries to connect:

  while (1) {
  newsock = accept(listensock, NULL,NULL);

Once a client connects, a new thread is started. The descriptor for the new client socket is passed to the thread function. Since the descriptor is passed to the function instead of being copied, there will be no need for the parent thread to close the descriptor:

  result = pthread_create(&thread_id, NULL, thread_proc, (void *) newsock);
  if (result != 0) {
    printf("Could not create thread.\n");
  }

Since the parent thread will be in a continuous loop, there will be no need to ever join one of the child threads. Therefore, we callpthread_detach()to keep zombies from occurring. A zombie is a process (or thread, in this case) that has returned and is waiting for its parent to check its return value. The system will keep the zombies around until the return value is checked, so they just take up resources. In our example, we arenít interested in the threadís return value, so we tell the system by callingpthread_detach(). Then, we callsched_yield()to give the new thread a chance to start execution by giving up the remainder of the parentís allotted time-slice.

    pthread_detach(thread_id);
    sched_yield();
  }
}
void* thread_proc(void *arg)
{
  int sock;
  char buffer[25];
  int nread;

In our thread function, we cast the passed argument back to a socket descriptor. Notice that we donít close the listening socket as we did inserver2.c. In a threaded server, the descriptors arenít copied to the child process, so we donít have an extra listening socket descriptor in the child. Next is the familiar routine: read characters, echo them to the screen and client, and close the connection.

  printf("child thread %i with pid %i created.\n", pthread_self(),
        getpid());
  sock = (int) arg;
  nread = recv(sock, buffer, 25, 0);
  buffer[nread] = '\0';
  printf("%s\n", buffer);
  send(sock, buffer, nread, 0);
  close(sock);
  printf("child thread %i with pid %i finished.\n", pthread_self(),
       
getpid());
}

The server can be compiled with the following command. Notice that we are linking with the pthread library. This is the library that gives us the threading capabilities.

gcc -o server4 -lpthread server4.c

Figure 5-8 shows a sample of the output obtained on executing the program. The client was run with five child processes.


Figure 5-8.  Output from a multithreaded server

Notice that, for a short time, all of the clients are connected at the same time.

Thread pools operate in a very similar manner to process pools. Our strategy is to create a certain number of threads when the application initializes. We then have a pool of threads to handle incoming client connections, and we avoid the costs associated with waiting to create a thread when the request is made. In addition, with shared memory, it is much easier to implement dynamic thread pools in which we can resize our thread pool at runtime depending on the load requirements.

On the downside, if one thread crashes, it can bring down the entire server application. This is due to the fact that all of the threads, including the main         application process, use the same memory space and resources (for example, file descriptors). So, for example, if one of the threads encounters a buffer overrun problem, it can corrupt memory being used by another thread. This is not the case with multiple processes, because the operating system prevents one process from writing over the memory in another process. Great care must be taken when designing a multithreaded server to prevent an errant thread from affecting the others.

Figure 5-9. shows the basic architecture for a prethreaded server application.


Figure 5-9.  Basic architecture for a prethreaded server application

In the following program (server5.c), the number of threads in the pool is passed in on the command line. The parent process then uses a loop to spawn the requested number of threads, passing the descriptor of the listening socket. It then callspthread_join()to keep it from returning before any of its threads. If we didnít insert this call, the parent process would end immediately and cause all its threads to return. Each thread then calls accept on the same listening socket and waits for a client connection. When a connection is made, the operating system chooses one of the threads to signal using a ďfirst in, first outĒ methodology. This thread receives the data from the client and echoes it back. Finally, the connection is closed, and the thread callsaccept()again to wait for another client.

These lines are very similar to the section in the programserver3.c:

/* server5.c */
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
void* thread_proc(void *arg);
int main(int argc, char *argv[])
{

First, we declare the variables that we will need.

  struct sockaddr_in sAddr;
  int listensock;
  int result;
  int nchildren = 1;
  pthread_t thread_id;
  int x;
  int val;

We check the command line to see how many threads should be in our thread pool. If none is specified, then we will create a single thread.

  if (argc > 1) {
    nchildren = atoi(argv[1]);
  }

Next, we create the socket that will listen for incoming connections.

  listensock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

Then, we set theSO_REUSEADDRoption.

val = 1;
result = setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
if (result < 0) {
       
perror("server5");
        return 0;
}

We bind it to a local port and to all addresses associated with the machine.

  sAddr.sin_family = AF_INET;
  sAddr.sin_port = htons(1972); 
  sAddr.sin_addr.s_addr = INADDR_ANY;
 
result = bind(listensock, (struct sockaddr *) &sAddr, sizeof(sAddr));
 
if (result < 0) {
    perror("server5");
    return 0;
 
}

We now put it into listening mode.

  result = listen(listensock, 5);
 
if (result < 0) {
    perror("server5");
    return 0;
 
}

Afterward, we create our pool of threads. Notice that we pass the descriptor for the listening socket instead of the client:

  for (x = 0; x < nchildren; x++) {
    result = pthread_create(&thread_id, NULL, thread_proc,
                           (void *) listensock);
    if (result != 0) {
     
printf("Could not create thread.\n");
    }
    sched_yield();
 
}

Here, we callpthread_join(). This has the same effect that callingwait()did inserver3.c. It keeps the parent thread from continuing until the child threads are finished:

  pthread_join (thread_id, NULL);
}
void* thread_proc(void *arg)
{
  int listensock, sock;
  char buffer[25];
  int nread;
 
listensock = (int) arg;
 
while (1) {

Each thread callsaccept()on the same listening socket descriptor. Just as inserver3.c, when a client connects, the kernel will choose a thread in whichaccept()returns:

  sock = accept(listensock, NULL, NULL);

Onceaccept()returns, we read the data from the client and echo it back. Then we close the connection.

    printf("client connected to child thread %i with pid %i.\n",
         
pthread_self(), getpid());
    nread = recv(sock, buffer, 25, 0);
    buffer[nread] = '\0';
    printf("%s\n", buffer);
    send(sock, buffer, nread, 0);
    close(sock);
    printf("client disconnected from child thread %i with pid %i.\n",
         
pthread_self(), getpid());
  }
}

The server can be compiled with the following command:

  gcc -o server5 -lpthread server5.c

This will run the server with five threads in the thread pool:

  ./server5 5

Figure 5-10 shows a sample of the output obtained on executing the program. The client was run with five child processes.


Figure 5-10.  Output from a prethreaded server

Notice that the threads are used in the order in which they calledaccept(). Since we have more clients than threads in the thread pool, earlier threads are reused for new clients once they become free.



 
 
>>> More Site Administration Articles          >>> More By Apress Publishing
 

blog comments powered by Disqus
escort Bursa Bursa escort Antalya eskort
   

SITE ADMINISTRATION ARTICLES

- Coding: Not Just for Developers
- To Support or Not Support IE?
- Administration: Networking OSX and Win 7
- DotNetNuke Gets Social
- Integrating MailChimp with Joomla: Creating ...
- Integrating MailChimp with Joomla: List Mana...
- Integrating MailChimp with Joomla: Building ...
- Integrating MailChimp with Joomla
- More Top WordPress Plugins for Social Media
- Optimizing Security: SSH Public Key Authenti...
- Patches and Rejects in Software Configuratio...
- Configuring a CVS Server
- Managing Code and Teams for Cross-Platform S...
- Software Configuration Management
- Back Up a Joomla Site with Akeeba Backup

Developer Shed Affiliates

 


Dev Shed Tutorial Topics: