Concurrent Servers Process, fork & threads ECE 297 ECE 297 Process-based server How do you handle cache updates? How do you handle cache invalidation? Cache Keep it simple Cache Cache Cache ECE 297 Cache Process-based server How do you handle concurrent access to files? Careful with writing to the same file in different processes! file ECE 297 Process versus thread I Process • Unit of resource ownership with respect to the execution of a single program • Can encompass more than one thread of execution – E.g., Web browser: More than one thread (process) per window/tab, GUI, rendering engine etc. – E.g., Web server: More than one thread for handling requests Thread • Unit of execution • Belongs to a process • Can be traced (i.e., list the sequence of instructions) ECE 297 Process versus thread II • A.k.a. lightweight process (LWP), threads, multithreaded processes ECE 297 Process versus thread III Per process items • Address space • Global variables • Open files • Child processes • Pending alarms • Signal and signal handlers • Accounting information Per thread items • Program counter • Registers • Stack ECE 297 Process 1 Process 2 Process 3 Process Threads Threads OS OS Use • Threads are part of the same “job” and are actively and closely cooperating Use • Processes are largely independent and often compete for resources ECE 297 Threads Process Thread 1’s stack Threads OS ECE 297 Thread-based server • Server design alternatives – Thread-per-request – Thread-per-client – Thread-per connection • The new thread can access all resources held by the process that created it • For example, the cache, open data files, global variables are all available to the threads – Unlike for process-based servers p is for POSIX pthreads API overview • pthread_create(…): creates a thread • pthread_wait(…): waits for a specific thread to exit • pthread_exit(…): terminates the calling thread • pthread_yield(…): calling thread passes control voluntarily to another thread ECE 297 p is for POSIX pthreads API I #include <pthread.h> Thread ID Thread priority, initial stack size, …; NULL for defaults pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func) (void *), void *arg); Pointer to argument for function Function to execute; the actual “thread” Returns 0, if OK, positive Exx on error ECE 297 p is for POSIX pthreads API IV • pthread_self(void) – Returns thread ID to caller • pthread_detach(pthread_t thread) – Indicates to system that storage for thread can be reclaimed • There are many other pthread API calls, the above should suffice for our purposes ECE 297 Thread-based server void *thread(void *vargp); int *connfdp; int main(int argc, char **argv) { … pthread_t tid; … listenfd = socket(…); … listen(listenfd, …) // main server loop for( ; ; ) { connfdp = malloc(sizeof(int)); … *connfdp = accept(listenfd, (struct sockaddr *) &clientaddr, &clientlen); pthread_create(&tid, NULL, thread, (void *) connfdp); } // for } // main ECE 297 We create the thread to handle the connected client. The actual thread to handle the client void *thread(void *vargp) { int connfd; // detached to avoid a memory leak pthread_detach(pthread_self()); connfd = *((int *)vargp); free(vargp); // do the work, service the client close(connfd); return NULL; This is where the client gets serviced } ECE 297 listenfd = socket(AF_INET, SOCK_STREAM, 0) … bind(listenfd, …) listen(listenfd, …) Concurrent server template for( ; ; ){ … connfd = accept(listenfd, …); … if ( (childPID = fork()) == 0 ){// The Child! close(listenfd); //Close listening socket do the work //Process the request exit(0); } … close(connfd); //Parent closes connfd } ECE 297 Issues with thread-based servers • Must be careful to avoid unintended sharing of variables • For example, what happens if we pass the address of connfd to the thread routine? pthread_create(&tid, NULL, thread, (void *)&connfd); • Must protect access to intentionally shared data Would be a shared variable – Here, we got around this by creating a new variable, but in general … ECE 297 Complications ! • Imaging a global variable counter in the process – For example the storage server in-memory cache (more complex structure) – Or the connfd variable Let’s dissect the issue in detail Shared data & synchronization Table Table Table What happens if multiple threads concurrently access shared process state (i.e., memory)? ECE 297 Concurrently manipulating shared data • Two threads execute concurrently as part of the same process • Shared variable (e.g., global variable) – counter = 5 • Thread 1 executes counter – counter++ • Thread 2 executes – counter— • What are the possible values of counter after Thread 1 and Thread 2 executed? ECE 297 Machine-level implementation • • Implementation of “counter++” register1 = counter register1 = register1 + 1 counter = register1 Implementation of “counter--” register2 = counter register2 = register2 – 1 counter = register2 ECE 297 Possible execution sequences counter++ Context Switch counter-- Context Switch Context Switch Context Switch ECE 297 counter-- Context Switch counter++ Interleaved execution • Assume counter is 5 and interleaved execution of counter++ (P) and counter– (C) T1: r1 = counter (register1 = 5) T1: r1 = r1 + 1 (register1 = 6) T2 : r2 = counter (register2 = 5) context T2 : r2 = r2– 1 (register2 = 4) switch T1 : counter = r1 (counter = 6) T2 : counter = r2 (counter = 4) • The value of counter may be either 4 or 6, where the correct result should be 5. ECE 297 Race condition • Race condition: – Several threads manipulate shared data concurrently. The final value of the data depends upon which thread finishes last. • In our example (interleaved execution) for c++ last, result would be 6, and for c-- last, result would be 4 (correct result should be 5) • To prevent race conditions, concurrent processes must be synchronized. ECE 297 The moral of this story • The statements counter++; counter--; must each be executed atomically. • Atomic operation means an operation that completes in its entirety without interruption. • This is achieved through synchronization primitives (semaphores, locks, condition variables, monitors, disabling of IRPs …). ECE297 Synchronization primitives • • • • Semaphore (cf. ECE344) Monitor (cf. ECE344) Condition variable (cf. ECE344) Lock – Prevent data inconsistencies due to race conditions – A.k.a. mutex (mutual exclusion) – Use to protect shared data within a process – Can not be used across processes • Need to use semaphore instead ECE 297 Mutex: Mutual exclusion pthread_mutex_lock(pthread_mutex_t *mtpr) pthread_mutex_unlock(pthread_mutex_t *mtpr) Returns 0, if OK, positive Exx on error • There are other abstractions, but the mutex should suffice for us • NB: In ECE344 we learn how to implement locks. ECE 297 The pthreads mutex (lock) pthread_mutex_t my_cnt_lock = PTHREAD_MUTEX_INITIALIZER; int counter=0; pthread_mutex_lock( & my_cnt_lock ); counter++; pthread_mutex_unlock( & my_cnt_lock ); … ECE 297 Mutex is for mutual exclusion pthread_mutex_t my_cnt_lock = PTHREAD_MUTEX_INITIALIZER For statically allocated mutexes. pthread_mutex_lock(& my_cnt_lock); Guaranteed to execute counter--; atomically pthread_mutex_unlock(& my_cnt_lock); pthread_mutex_lock(& my_cnt_lock); Guaranteed to execute atomically counter++; pthread_mutex_unlock(& my_cnt_lock); ECE 297 Possible execution sequences lock lock lock counter++ lock counter-unlock unlock Context Switch Context Switch lock lock lock counter-- counter++ lock unlock unlock ECE 297 Watch out for I • For all shared data access you must use a synchronization mechanism • For Milestone 4 based on threads, you can get by with the mutexes • Other useful mechanisms in pthreads are – pthread_join(…) – pthread_cond_wait(…) & pthread_cond_signal() • Bugs due to race conditions are extremely difficult to track down – Non-deterministic behaviour of code ECE 297 Watch out for II • You can not make any assumption about thread execution order or relative speed • Threaded code must use thread-safe functions – Functions that use no static variables, no global variables, don’t return pointers to static variables • Otherwise need to protect call to non-thread-safe code with mutexes • Non-thread-safe code also called non-reentrant code – Function local data is allocated on the stack • Deadlocks – Code halts, as threads may wait indefinitely on locks – Cause is programmer error or poorly written code ECE 297 Pros & cons of threads-based servers • Probably the simplest option –No zombies, no signal handling, no onerous data structures • “Easy” to share data structures between threads –Logging information, data files, cache, … • Thread creation is more efficient than process creation • Enables concurrent processing of requests from multiple clients ECE 297 Pros & cons cont.’d • Unintentional sharing can introduce subtle and hard to reproduce race conditions • malloc an argument (struct) for each thread and pass pointer to variable to thread and free after use • Keep global variables to a minimum • If a thread references a global variable • protect it with a mutex or • think carefully about whether unprotected variable is safe –e.g., one writer thread vs. multiple readers is OK. ECE 297