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
- Main Loop: The main thread blocks on
accept(). - Creation: When a client connects,
accept()returns a connected descriptor (connfd). The main thread immediately callspthread_create(). - The Handler (
doit): The new thread executes a helper function (often calleddoit), which callsstr_echo(the same function we used in the process-based server). - 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)whenstr_echoreturns.
- Detaching:* The thread calls
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:
- Thread A is created with a pointer to
connfd. Let’s say now its contain the file descriptor value of 5. - The Main Thread loops immediately and calls
acceptagain. The new stored file descriptor value is 6 for example. - A new client connects, and the Main Thread overwrites the value in
connfdwith the new descriptor. - 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 ValueThis 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_createfunction 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:
- Main Thread:
Malloca small integer space. - Main Thread: Copy
connfdinto that memory. - Main Thread: Pass the pointer to that new memory to
pthread_create. - Worker Thread: Copy the value out.
- Worker Thread:
Freethe memory.
/* Portable Argument Passing */
int *iptr;
iptr = Malloc(sizeof(int));
*iptr = connfd;
Pthread_create(&tid, NULL, &doit, iptr);