Skip to main content

Network services: Legacy design versus threaded design

Should you create worker processes or worker threads?

Chris Herborth (chrish@pobox.com), Freelance, Freelance Writer
Photo of Chris Herborth
Chris Herborth is an award-winning senior technical writer with more than 10 years of experience writing about operating systems and programming. When he's not playing with his son Alex or hanging out with his wife Lynette, Chris spends his spare time designing, writing, and researching (that is, playing) video games.

Summary:  So, you've got a great idea for a new network service that'll change the world, and you've just finished your first set of socket programming tutorials. Now you've just got to design the thing and finish off a test implementation, right? Traditionally, programs like this use the venerable UNIX®fork() system call to handle connections in a child process, but this is slow and inefficient, even on modern UNIXes. In this article, you'll get a look at using POSIX threads instead of child processes, and you'll also get an introduction to threaded programming -- a topic many UNIX programmers haven't encountered before.

Date:  14 Feb 2006
Level:  Intermediate
Activity:  1077 views

Introduction

While brainstorming or drinking coffee one day, you've thought up a great idea for a new networked service -- something that could change the world, or at least the Internet. Unfortunately, you don't have a clue where to start. You read some socket programming tutorials and learn all about how you could write your service as a single processing loop using the select() call to manage handling multiple connections.

But wait, that sounds a lot like co-operative multitasking, which certainly isn't the way things should work on a modern operating system. You definitely don't want to waste your time keeping track of what's going on with the various streams yourself.

Luckily for you, UNIX® lets you create child processes to do the work. Also, modern UNIXes let you use POSIX threads, too. Read on to find out what these are and see how you can use them to do several things at the same time.

Splitting your process with fork()

Before threads and lightweight processes were invented on UNIX systems, a simple system call, fork(), was used to create multiple streams of execution by splitting off an identical child process like binary osmosis. These "forked" processes share the same code, but get a private copy of the original's current data, including open files, sockets, security privileges, and any other artifacts the implementation deemed fit.

This sounds like a great solution to the problem of needing to handle multiple network connections simultaneously; just fork() off a child process after you've accepted a connection and let it handle protocol negotiation and data transfers while the original process waits for a new connection. See Figure 1 below.


Figure 1. Forked processes
building Geronimo

Listing 1 shows how a program using fork() is laid out.


Listing 1. General fork() program layout
 
#include <unistd.h>
#include <sys/types.h>

int main( int argc, char **argv )
{
    pid_t child = 0;

    child = fork();
    if( child < 0 ) {
        /* An error has occurred! */
        handle_error();
    } else if( child == 0 ) {
        /* Parent process. */
		
        handle_parent_duties();
    } else {
        /* Child process. */
        handle_child_duties();
    }

    return 0;
}

Why wouldn't you want to use fork()?

  • Switching between processes can be expensive due to context switching overhead (saving and restoring registers and possibly swapping part of the program back into memory). Also, your system's scheduler might limit the number of processes that can be active. It might even stop being effective past a certain point -- the system might spend more time figuring out which process to run than actually running the processes.
  • The inherited open files and sockets could result in unexpected synchronization problems if you're not careful after calling fork(). You'll probably want to use dup() to make private copies of file or socket descriptors -- this results in additional overhead.
  • Creating a copy of all a program's data might take a while. It might also waste memory that you need for running processes.
  • If the parent and child, or individual children, need to communicate or share data, inter-process synchronization techniques are generally very slow.

Now that you know why you wouldn't want to use fork(), let's talk about threads and go over some code samples.

Going light with threads

Because of fork()'s shortcomings, UNIX developers started inventing more specialized versions that would be more efficient in suitable situations. Eventually, this resulted in threads being added to the kernel as a lightweight execution stream. Processes were now defined as one or more threads of execution instead of just a stream of instructions.

When a process creates a new thread, the thread is a part of the process. All of the open files and sockets are available to the thread. More importantly, so is all of the data of the process, making communication and synchronization between threads simple and efficient (see Figure 2 and Listing 2).


Figure 2. Threads in a process
building Geronimo

Listing 2. General threaded program layout

#include <pthread.h>
#include <stddef.h>

void *thread_function( void *data )
{
    /* Do something useful. */

    return NULL;
}

int main( int argc, char **argv )
{
    pthread_t threadId = 0;
    pthread_attr_t attr;

    pthread_attr_init( &attr );
    pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_DETACHED );
	
    (void)pthread_create( &threadId, &attr, &thread_function, NULL );

    /* Do other things while the worker thread is running. */

    return 0;
}

As you can see, creating a thread requires a bit more setup than just calling fork() did, but it offers a lot more flexibility and control.

Why wouldn't you want to use threads?

  • You need to use thread-safe libraries or carefully orchestrate calls to functions that maintain state between calls. Traditional UNIX functions that maintain state, such as rand(), which keeps internal data in a static variable, have thread-safe replacements, rand_r() in this case, in systems that support POSIX threads. Because thread-safe libraries are generally available, this isn't a drawback unless you forget to use the thread-safe version of a function. In which case, you'll experience odd behavior in your application and might even have a hard time debugging the problem.
  • Debugging thread deadlocks caused by improper use of synchronization techniques, such as mutexes and condition variables, can be painful.
  • Bad programming habits, such as global variables and static variables inside functions, must be abandoned in favor of writing cleaner, modern code.

As you've seen the basic structure of a forking program and a threaded program, let's create two complete programs illustrating each approach.

A word about the code

You can download the code from this article from the Downloads section. After that, you can import the code and projects (which are managed make projects) through Eclipse's File>Import command. Eclipse is an excellent platform and language-neutral integrated development environment with loads of helpful third-party plug-ins. For more information about Eclipse, see the Resources section.

Give me the code

In this rather contrived example, you need to generate a bunch of random numbers. You wouldn't normally go through the trouble of launching child processes or threads to do something like this, but the same approach can be applied to:

  • Long I/O operations
  • Network services
  • Log analysis
  • Cryptography
  • Compression

Any task that has multiple steps can benefit from this division of labor, as long as two or more of those steps can occur at the same time.

In addition to generating (and then discarding) some random numbers, both of the programs illustrate "good form" for handling child processes and threads.

Forked workers

The forked approach requires a signal handler to catch when child processes exit. When it's finished forking child processes, it waits for any remaining children to exit before it returns from its own main() function.

In Listing 3, you can see the standard headers required to declare the functions and other symbols referenced by this program. During linking, you won't need to include any additional libraries, as you're using standard C library functions. You're going to use 32 separate workers; so, counting the parent, you'll have a total of 33 processes running just for this one task. Additionally, you're creating a global variable to count the number of worker processes that are still running. You'll need that at the end of the main() function in order to exit cleanly.


Listing 3. Headers and declarations

#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define FORK_WORKERS 32

volatile static int activeWorkers = 0;

Here's the trivial work function (see Listing 4). Unless you want to follow along with the example code download, feel free to replace this with something more interesting. Note how everything is being done in this function, including generating random numbers with rand_r(). Because rand() isn't thread-safe, the code uses rand_r() instead of the standard rand(). It maintains state internally (using a static variable) -- a definite mistake for threaded code. This isn't important in a forked program, but it's still good form.


Listing 4. The worker function

void fork_worker( void )
{
    unsigned int randState = 0;
    int idx = 0;
	
    randState = (unsigned int)getpid();
	
    for( idx = 0; idx < ( FORK_WORKERS * FORK_WORKERS ); idx++ ) {
        int val = rand_r( &randState );
        if( val > randState ) {
            val++;
        } else {
            val--;
        }
    }
	
    exit( EXIT_SUCCESS );
}

This short function gets called by the SIGCHLD signal handler you're going to create down in the fork() function. The UNIX process manager sends a SIGCHLD signal to the parent process whenever one of its children exits. This gives you a chance to grab the child's exit status using the wait() function. You need to wait for the children, so they don't turn into zombie processes. Zombies are the remains of programs that have exited -- maybe they're just a process ID and an exit status, or maybe the process manager is caching additional process data. If you don't clean up zombies by using the wait() functions (wait() and waitpid(), which lets you wait for a specific process ID), your process table will fill up and you eventually won't be able to create new processes.

Signal handlers can't do much, because most functions are forbidden in a signal handler. Luckily, the wait() function is one of the few signal handler-safe functions in the standard C library, and it's generally used exactly like you're doing here.

If a process isn't waiting to have its unnatural life as a zombie process ended, the wait() function will, as the name suggests, block until a child process exits. Since the signal handler shown in Listing 5 is being called in response to a child exiting, this call to wait() is going to exit immediately.


Listing 5. The signal handler
 

void worker_exitted( int sig )
{
    int status;
	
    activeWorkers--;
    (void)wait( &status );
}

After declaring some variables, Listing 6 shows you how to use sigaction() to set up the signal handler you'll need to prevent zombie processes.

The for loop is where the interesting bits are happening. When you call fork(), it splits your process into two identical processes, the original process (the parent) and the child. For the parent, it returns the process ID of the child and, in the child, it returns 0. If an error occurs while the child is being created, it returns a negative error code.

The if statement following the fork() call handles the possibility of error, sends the child process off to the fork_worker() function you looked at earlier, or counts the number of active children if it's the parent process running.

Before fork() exits, make sure that none of the child processes are still in a zombie state. At this point, the wait() calls will block if there are still child processes doing work (see Listing 6).


Listing 6. The mainline
 

int main( int argc, char **argv )
{
    pid_t ppid = 0;
    int idx = 0;
    struct sigaction signalHandler;
    sigset_t mask;
	
    (void)sigemptyset( &mask );
    signalHandler.sa_handler = worker_exitted;
    signalHandler.sa_mask = mask;
    signalHandler.sa_flags = 0;

    (void)sigaction( SIGCHLD, &signalHandler, NULL );
	
    printf( "Launching %d child processes...\n", FORK_WORKERS );

    /* Create the child processes */
    for( idx = 0; idx < FORK_WORKERS; idx++ ) {
        ppid = fork();
        if( ppid < 0 ) {
             printf( "Error creating child %d:  %s\n", idx, 
                     strerror( errno ) );
        } else if( ppid == 0 ) {
            /* I'm a child process. */
            fork_worker();
        } else {
            /* Parent process. */
            printf( "Child %d's PID is %d\n", idx, ppid );
            activeWorkers++;
        }
    }

    sleep( 1 );

    /* Wait for children to exit */
    printf( "Waiting for %d children to exit...\n", activeWorkers );
    while( activeWorkers > 0 ) {
        int status = 0;
        if( wait( &status ) == -1 ) {
            /* All children have exited already. */
            break;
        }
        activeWorkers--;
		
        if( WIFSIGNALED( status ) ) {
            printf( "Child exitted due to signal %d\n", WTERMSIG( status ) );
        } else {
            printf( "Child's exit status was %d\n", WEXITSTATUS( status ) );
        }
    }
	
    printf( "Done!\n" );
	
    return EXIT_SUCCESS;
}

The last interesting detail here is the process exit status that you get from wait() isn't the value returned by the process with a call to exit(), or any of the other methods of exiting. It's got additional data encoded in its bits, so you need to use the WIFSIGNALED() macro to get the true exit status. Note the spelling, I'm always adding an extra L in there.

Threaded workers

Even though the threaded version looks more complex, it's actually more straightforward. The complexity of the POSIX threads API is significantly more flexible than the legacy fork() and wait() functions.

As before, you can include a few system headers and define the number of worker threads you'll launch. Note the lack of global variables.

You can also define EOK, because not all errno.h headers have a "no error" value defined (see Listing 7).


Listing 7. Headers and declarations
 

#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
#define THREAD_WORKERS 32

#if !defined(EOK)
#  define EOK 0
#endif

In Listing 8, you find the thread_worker() function, which is nearly identical to the fork_worker() function you looked at earlier. You use rand_r() here so the other threads don't mess with rand()'s state while you're generating random numbers. Your system manual describes a "_r" version for any library function that keeps state between calls.


Listing 8. Worker thread
 

void *thread_worker( void *data )
{
    unsigned int randState = 0;
    int idx = 0;
	
    randState = (unsigned int)getpid();
	
    for( idx = 0; idx < ( THREAD_WORKERS * THREAD_WORKERS ); idx++ ) {
        int val = rand_r( &randState );
        if( val > randState ) {
            val++;
        } else {
            val--;
        }
    }
	
    return NULL;
}

The program's mainline creates the threads and then waits for them to finish their work before exiting. You don't need to make a signal handler, because threads don't live forever in a zombie state.

The call to pthread_create() creates a new thread and then starts it running. That last detail might be important if you need to do additional setup before letting the thread continue; use a mutex or some other synchronization primitive to block the new thread.

The final for loop (see Listing 9) simply goes through all of the thread IDs and uses pthread_join() to wait for each thread to finish and exit. Nothing special is required to get the thread's exit value.


Listing 9. Mainline

int main( int argc, char **argv )
{
    pthread_t threadIds[THREAD_WORKERS];
    int idx = 0;
    int retval = 0;

    printf( "Creating %d child threads...\n", THREAD_WORKERS );

    /* Create and launch the child threads */
    for( idx = 0; idx < THREAD_WORKERS; idx++ ) {
        retval = pthread_create( &(threadIds[idx]), NULL, 
                                 &thread_worker, NULL );
								  
        printf( "Child %d's thread ID is %d\n", idx, (int)threadIds[idx] );
    }
	
    sleep( 1 );
	
    /* Wait for children to exit */
    for( idx = 0; idx < THREAD_WORKERS; idx++ ) {
        int status = 0;
        retval = pthread_join( threadIds[idx], (void *)&status );
		
        if( retval == 0 ) {
            printf( "Thread %d's exit status was %d\n", 
					(int)threadIds[idx], status );
        } else {
            printf( "Error joining thread %d\n",
                    (int)threadIds[idx] );
        }
    }
	
    printf( "Done!\n" );
	
    return EXIT_SUCCESS;
}

With threads, you have to be careful with shared resources, such as global variables and functions, that save state in static variables. However, by using threads, you gain a more logical way of taking advantage of multiple processors and partitioning your application's work into straightforward, lightweight execution streams.

Summary

One of the problems you'll run into while developing something like a network service on UNIX is how to handle the need to continue accepting connections while serving clients that are already connected. How can you continue to work while waiting for something else to happen?

The legacy approach is to use the standard UNIX fork() function to split your process into an identical parent and child process. The parent can go back to waiting for another connection while the child goes off and services the client. Until the child exits, the parent is interrupted with a signal. If you don't clean up your child processes properly, your process table fills up with zombies, preventing additional processes from being created.

Threaded programming is a new thing to many UNIX programmers, but it offers a much cleaner way of handling more than one thing at a time. Your process creates one or more threads to do its bidding; the mainline can go back to waiting for connections while the threads handle the clients without additional supervision. Special cleanup and signal handling code isn't necessary. Your program isn't going to unintentionally keep you from handling new connections by filling up the process table.



Downloads

DescriptionNameSizeDownload method
Eclipse 3.1 project demonstrating fork()es-forkWork.zip23KB FTP | HTTP | Download Director
Eclipse 3.1 project demonstrating threadses-threadWork.zip23KB FTP | HTTP | Download Director

Information about download methods


Resources

Learn

Discuss

About the author

Photo of Chris Herborth

Chris Herborth is an award-winning senior technical writer with more than 10 years of experience writing about operating systems and programming. When he's not playing with his son Alex or hanging out with his wife Lynette, Chris spends his spare time designing, writing, and researching (that is, playing) video games.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=AIX and UNIX, Open source
ArticleID=103922
ArticleTitle=Network services: Legacy design versus threaded design
publish-date=02142006
author1-email=chrish@pobox.com
author1-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Special offers