CS 208 s22 — Concurrency
1 Why Concurrency?
- concurrent computing: when the execution of multiple computations (or processes) overlap
- we need to correctly control access to shared resources
We've seen that the kernel can overlap the execution of processes. But user applications can also make use of concurreny. Benefits of application-level concurrency:
- Accessing slow I/O devices
- Interacting with humans
- Reducing latency by deferring work
- Servicing multiple network clients
- Computing in parallel on a multi-core machine
2 Process-based Concurrency
int main(int argc, char **argv) { int listenfd, connfd; socklen_t clientlen; struct sockaddr_storage clientaddr; listenfd = Open_listenfd(argv[1]); while (1) { clientlen = sizeof(struct sockaddr_storage); connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen); echo(connfd); Close(connfd); } exit(0); }
#include "csapp.h" void sigchld_handler(int sig) { while (waitpid(-1, 0, WNOHANG) > 0) ; return; } int main(int argc, char **argv) { int listenfd, connfd; socklen_t clientlen; struct sockaddr_storage clientaddr; Signal(SIGCHLD, sigchld_handler); listenfd = Open_listenfd(argv[1]); while (1) { clientlen = sizeof(struct sockaddr_storage); connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen); if (Fork() == 0) { Close(listenfd); /* Child closes its listening socket */ echo(connfd); /* Child services client */ Close(connfd); /* Child closes connection with client */ exit(0); /* Child exits */ } Close(connfd); /* Parent closes connected socket (important!) */ } }
- When
fork
creates a child process, the child gets a copy of the parent's file descriptor table. - This means the child has both
connfd
andlistenfd
. - The child has no need of the listening socket, so it should close it (it remains open in the parent).
- Parent process must close its copy of
connfd
- Kernel keeps reference count for each socket/open file
- After
fork
, the reference count forconnfd
is 2 - Connection will not be closed until
refcnt(connfd) = 0
- Listening server process must reap zombie children
- When a child process stops or terminates, the parent is sent a
SIGCHLD
signal - The default response is to ignore it
- But this could result in these zombie child processes hanging around, taking up memory, waiting to communicate their exit status to the parent
- So to avoid fatal memory leak, the parent should immediately call
wait
(or an equivalent) to reap (deallocate) the child
- When a child process stops or terminates, the parent is sent a
2.1 Pros and Cons of Process-based Servers
- Pros
- Handle multiple connections concurrently.
- Clean sharing model.
- descriptors (no)
- file tables (yes)
- global variables (no)
- Simple and straightforward.
- Cons
- Additional overhead for process control.
- Nontrivial to share data between processes. (This example too simple to demonstrate)
3 Threads
- previously, we defined a process as an instance of a running program
- a thread is a single execution sequence that represents the minimal unit of scheduling
- one process may contain multiple threads
- Multiple threads can be associated with a process
- Each thread has its own logical control flow
- Each thread shares the same code, data, and kernel context
- Each thread has its own stack for local variables but not protected from other threads
- Each thread has its own thread id (TID)
3.1 Process vs Threads
- How threads and processes are similar
- Each has its own logical control flow
- Each can run concurrently with others (possibly on different cores)
- Each is context switched
- How threads and processes are different
- Threads share all code and data (except local stacks)
- Processes (typically) do not
- Threads are somewhat less expensive than processes
- Process control (creating and reaping) twice as expensive as thread control
- Linux numbers:
- ~20K cycles to create and reap a process
- ~10K cycles (or less) to create and reap a thread
3.2 Threading Models
- POSIX Threads (pthread)
#include <pthread.h>
, low-level interface giving fine-grained control- Standard interface for ~60 functions that manipulate threads from C programs
- Creating and reaping threads
pthread_create()
pthread_join()
- Determining your thread ID
pthread_self()
- Terminating threads
pthread_cancel()
pthread_exit()
exit()
[terminates all threads]return
[terminates current thread]
- Creating and reaping threads
- Fork-Join model (OpenMP)
#include <omp.h>
, higher-level interface for fork-join approach- pthreads can do fork-join manually
3.3 Threaded Echo Server
#include "csapp.h" /* Thread routine */ void *thread(void *vargp) { int connfd = *((int *)vargp); Pthread_detach(pthread_self()); Free(vargp); echo(connfd); Close(connfd); return NULL; } int main(int argc, char **argv) { int listenfd, *connfdp; socklen_t clientlen; struct sockaddr_storage clientaddr; pthread_t tid; listenfd = Open_listenfd(argv[1]); while (1) { clientlen=sizeof(struct sockaddr_storage); connfdp = Malloc(sizeof(int)); *connfdp = Accept(listenfd, (SA *) &clientaddr, &clientlen); Pthread_create(&tid, NULL, thread, connfdp); } return 0; }
- Run thread in detached mode.
- Runs independently of other threads
- Reaped automatically (by kernel) when it terminates
- Free storage allocated to hold
connfd
. - Close
connfd
(important!) - Must be careful to avoid unintended sharing
- For example, passing pointer to main thread’s stack
Pthread_create(&tid, NULL, thread, (void *)&connfd)
- For example, passing pointer to main thread’s stack