The goal here is to replace the fork() system call with pthread_create().

  • Process Model (Old): Parent accepts connection fork() Child handles client / Parent closes connection.
  • Thread Model (New): Main thread accepts connection pthread_create() New thread handles client. This section highlights two critical implementation details: how to manage file descriptors in a shared environment and how to safely pass arguments to new threads.
1. The Basic Implementation (Figure 26.3)

The book first presents a “basic” version of the threaded server.

#include    "unpthread.h"
 
static void *doit(void *);      /* each thread executes this function */
 
int
main(int argc, char **argv)
{
    int     listenfd, connfd;
    pthread_t tid;
    socklen_t addrlen, len;
    struct sockaddr *cliaddr;
 
    if (argc == 2)
        listenfd = Tcp_listen(NULL, argv[1], &addrlen);
    else if (argc == 3)
        listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
    else
        err_quit("usage: tcpserv01 [ <host> ] <service or port>");
 
    cliaddr = Malloc(addrlen);
 
    for ( ; ; ) {
        len = addrlen;
        connfd = Accept(listenfd, cliaddr, &len);
        Pthread_create(&tid, NULL, &doit, (void *) connfd);
    }
}
 
static void *
doit(void *arg)
{
    Pthread_detach(pthread_self());
    str_echo((int) arg);        /* same function as before */
    Close((int) arg);           /* done with connected socket */
    return (NULL);
}

The Logic Flow

  1. Main Loop: The main thread blocks on accept().
  2. Creation: When a client connects, accept() returns a connected descriptor (connfd). The main thread immediately calls pthread_create().
  3. The Handler (doit): The new thread executes a helper function (often called doit), which calls str_echo (the same function we used in the process-based server).
  4. Cleanup:
    • Detaching:* The thread calls pthread_detach(pthread_self()). This is crucial. If we don’t detach, the thread’s resources (stack, ID) hang around as “zombies” waiting to be joined. Since the main server doesn’t want to wait for every client to finish, we detach so resources are freed automatically upon termination.
    • Closing: The thread calls close(connfd) when str_echo returns.
Key Difference: Descriptor Handling

In the fork() model, the parent process must close the connfd so that the reference count drops to zero eventually. In the Thread model, the main thread must NOT close connfd. Threads share the same file descriptor table. If the main thread closes connfd, it closes it for the worker thread too, instantly cutting off the client.

2. The Argument Passing “Race Condition”

This is the most important technical lesson in this section. Stevens highlights a common bug when passing the file descriptor to the new thread.

The Bug (What NOT to do)

/* DON'T DO THIS */
int connfd;
...
connfd = Accept(listenfd, ...);
Pthread_create(&tid, NULL, &doit, &connfd); // Passing address of connfd
 

If you pass the address of connfd (&connfd), you create a race condition. Scenario:

  1. Thread A is created with a pointer to connfd. Let’s say now its contain the file descriptor value of 5.
  2. The Main Thread loops immediately and calls accept again. The new stored file descriptor value is 6 for example.
  3. A new client connects, and the Main Thread overwrites the value in connfd with the new descriptor.
  4. Thread A finally wakes up and reads the value from the pointer &connfd. It now sees the new client’s descriptor, not the old value of 5 (the previous file descriptor).
Solution A: The “Cast” (Figure 26.3)

The code in Figure 26.3 uses a common trick: casting the integer descriptor to a void pointer.

Pthread_create(&tid, NULL, &doit, (void *) connfd); // Pass Value

This passes the value of connfd directly. The new thread casts it back: int connfd = (int) arg;. This works on most Unix systems because an integer usually fits inside a pointer (e.g., 32-bit int, 64-bit pointer), but technically it’s not strict ANSI C compliance.

Note

We DO pass by value, but we have to “disguise” that integer as a pointer because the pthread_create function is hard-coded to only accept a pointer.

Solution B: The “Portable” Way (Figure 26.4)

For strict correctness (and to handle larger arguments), Figure 26.4 shows the portable approach using dynamic memory:

  1. Main Thread: Malloc a small integer space.
  2. Main Thread: Copy connfd into that memory.
  3. Main Thread: Pass the pointer to that new memory to pthread_create.
  4. Worker Thread: Copy the value out.
  5. Worker Thread: Free the memory.
/* Portable Argument Passing */
int *iptr;
iptr = Malloc(sizeof(int));
*iptr = connfd;
Pthread_create(&tid, NULL, &doit, iptr);