Concurrency, Threads, and Events Ken Birman (Based on a slide set prepared by Robbert van Renesse) Summary Paper 1 Using Threads in Interactive Systems: A Case Study (Hauser et al 1993) Analyzes two interactive computing systems Classifies thread usage Finds that programmers are still struggling (pre-Java) Limited scheduling support Priority-inversion Summary Paper 2 SEDA: An Architecture for WellConditioned, Scalable Internet Services (Welsh, 2001) Analyzes threads vs event-based systems, finds problems with both Suggests trade-off: stage-driven architecture Evaluated for two applications Easy to program and performs well What is a thread? A traditional “process” is an address space and a thread of control. Now add multiple thread of controls Share address space Individual program counters and stacks Same as multiple processes sharing an address space. Thread Switching To switch from thread T1 to T2: Thread T1 saves its registers (including pc) on its stack Scheduler remembers T1’s stack pointer Scheduler restores T2’ stack pointer T2 restores its registers T2 resumes Two models: preemptive/non-preemptive Thread Scheduler Maintains the stack pointer of each thread Decides what thread to run next Decides when to pre-empt a running thread E.g., based on a timer May need to deal with multiple CPUs E.g., based on priority or resource usage But not usually “fork” creates a new thread Blocking or calling “yield” lets scheduler run Synchronization Primitives Semaphores P(S): block if semaphore is “taken” V(S): release semaphore Monitors: Only one thread active in a module at a time Threads can block waiting for some condition using the WAIT primitive Threads need to signal using NOTIFY or BROADCAST Uses of threads To exploit CPU parallelism To exploit I/O parallelism Run I/O while computing, or do multiple I/O Listen to the “window” while also running code, e.g. allow commands during an interactive game For program structuring Run two CPUs at once in the same program E.g., timers To avoid deadlock in RPC-based applications Hauser’s categorization Defer Work: asynchronous activity Pumps: pipeline components Print, e-mail, create new window, etc. Wait on input queue; send to output queue E.g., slack process: add latency for buffering Sleepers & one-shots Periodic activity & timers Categorization, cont’d Deadlock Avoiders Avoid deadlock through ordered acquisition of locks When needing more locks, roll-back and re-acquire Task Rejuvenation: recovery Start new thread when old one dies, say because of uncaught exception Categorization, cont’d Serializers: event loop Concurrency Exploiters for (;;) { get_next_event(); handle_event(); } Use multiple CPUs Encapsulated Forks Hidden threads used in library packages E.g., menu-button queue Common Problems Priority Inversion Deadlock X waits for Y, Y waits for X Incorrect Synchronization High priority thread waits for low priority thread Solution: temporarily push priority up (rejected??) Forgetting to release a lock Failed “fork” Tuning E.g. timer values in different environment Problems he neglects Implicit need for ordering of events E.g. thread A is supposed to run before thread B does, but something delays A Non-reentrant code Languages lack “monitor” features and users are perhaps surprisingly weak at detecting and protecting concurrently accessed data Criticism of Hauser He assumes superb programmers and seems to believe that “most” programmers won’t use threads (his example systems are really platforms, not applications) Systems old but/and not representative Pre-Java and C# And now there are some tools that can help discover problems What is an Event? An object queued for some module Operations: create_event_queue(handler) EQ enqueue_event(EQ, event-object) Invokes, eventually, handler(event-object) Handler is not allowed to block Blocking could cause entire system to block But page faults, garbage collection, … Example Event System (Also common in telecommunications industry, where it’s called “workflow programming”) Event Scheduler Decides which event queue to handle next. Never pre-empts event handlers! Based on priority, CPU usage, etc. No need for stack / event handler May need to deal with multiple CPUs Synchronization? Handlers cannot block no synchronization Handlers should not share memory At least not in parallel All communication through events Uses of Events CPU parallelism I/O concurrency Different handlers on different CPUs Completion of I/O signaled by event Other activities can happen in parallel Program structuring Not so great… But can use multiple programming languages! Hauser’s categorization ?! Defer Work: asynchronous activity Pumps: pipeline components Send event to printer, etc Natural use of events! Sleepers & one-shots Periodic events & timer events Categorization, cont’d Deadlock Avoiders Ordered lock acquisition still works Task Rejuvenation: recovery Watchdog events? Categorization, cont’d Serializers: event loop Concurrency Exploiters Natural use of events and handlers! Use multiple CPUs Encapsulated Events Hidden events used in library packages E.g., menu-button queue Common Problems Priority inversion, deadlock, etc. much the same with events Threaded Server Throughput Event-driven Server Throughput Threads vs. Events Events-based systems use fewer resources Event-based systems harder to program Better performance (particularly scalability) Have to avoid blocking at all cost Block-structured programming doesn’t work How to do exception handling? In both cases, tuning is difficult Both? In practice, many kinds of systems need to support both threads and events Threaded programs in Unix are the common example of these, because window systems use events The programmer uses cthreads or pthreads Major problem: the UNIX kernel interface wasn’t designed with threads in mind! Why does this cause problems? Many system calls block the “process” File read or write, for example And many libraries aren’t reentrant So when the user employs threads The application may block unexpectedly Limited work-around: add “kernel threads” And the user might stumble into a reentrancy bug Events as seen in Unix Window systems use small messages… But the “old” form of events are signals Kernel basically simulates an interrupt into the user’s address space The “event handler” then runs… But can it launch new threads? Some system calls can return EINTR Very limited options to “block” signals in critical sections How people work around this? They try not to do blocking I/O Use asynchronous system calls… or select… or some mixture of the two Or try to turn the whole application into an event-driven one using pools of threads, in the SEDA model (more or less) One dedicated thread per I/O “channel”, to turn signal-style events into events on the event queue for the processing stage This can be hard, but it works Must write the whole program and have a way to review any libraries it uses! One learns, the hard way, that pretty much nothing else works Unix programs built by inexperienced developers are often riddled with concurrency bugs! SEDA Mixture of models of threads and (small message-style) events Events, queues, and “pools of event handling threads”. Pools can be dynamically adjusted as need arises. Similar to Javabeans and EventListeners? SEDA Stage Authors: “Best of both worlds” Ease of programming of threads Or even better Performance of events Or even better Threads Considered Harmful Like goto, transfer to some entry in program In any scope Destroys structure of programs Primitive Synchronization Primitives Too low-level Too coarse-grained Too error-prone Prone to over-specification Example: create file 1. 2. 3. 4. Create file Read current directory (may be cached) Update and write back directory Write file Thread Implementations Serialize: op1; op2; op3; op4 1. Simplest and most common • Use threads 2. Requires at least two semaphores! Results in complicated program • • Simplified threads 3. a) b) c) • Create file and read directory in parallel Barrier Write file and write directory in parallel Over-specification! Event Implementation Create a dummy handler that awaits file creation and directory read events and then send an event to update the directory. Not great… GOP: Discussion Specifies dependencies at a high-level Can easily be supported by many languages C, Java, etc. Top-down specification No semaphores, condition variables, etc No explicit threads nor events cmp with make, prolog, theorem prover Exception handling easily supported Conclusion Threads still problematic Events also problematic As a code structuring mechanism High resource usage Hard to code, but efficient SEDA and GOP address shortcomings But neither can be said to have taken hold Issues not discussed Kernel vs. User-level implementation Shared memory and protection tradeoffs Problems seen in demanding applications that may launch enormous numbers of threads