Unformatted text preview: Notes adapted from Justin Chen, Chung Wu CS61A Notes – Week 6a: Concurrency, Streams A Concurrent March Through Programming Hell On your computer, you often have multiple programs running at the same time – you might have your internet browser open browsing questionable pictures, your P2P software downloading non
pirated software, and your instant messaging client lying to a clueless middle
schooler across the country. But you have only one computer, and one CPU! How can you do so many things at once? What actually happens is, the CPU switches between the different processes very quickly, doing work for each for a little while before moving on to the next, creating the illusion that the programs are running concurrently. The benefits are obvious for users like us so used to multitasking. Unfortunately, parallelism is one of the biggest headaches you’ll encounter. We’ll attempt to give you a tiny migraine, but in CS162, you’ll be swimming in a large ocean of pain with no shore in sight and a leaking life jacket. A bit of syntax. To run things concurrently, we use a Scheme primitive called parallelexecute, a procedure that takes in any number of “thunks” – procedures that take no arguments – and executes the thunks in parallel. For example, (define x 5) (parallelexecute (lambda () (set! x (+ x 10))) (lambda () (set! x (+ x 20)))) will attempt to set x to (+ x 10) and set x to (+ x 20) at the same time. Again, the computer “cheats” by interleaving operation between the different thunks. The answer that we want, of course, is 35 – we want the two thunks executed at the same time, but we still want the result to be as if they executed consecutively. Now, consider this simple Scheme expression: (set! x (+ x 10)) What looks like one Scheme operation is actually three operations: 1. lookup the value of x 2. add the value of x to 10 3. store the result into x Thus, consider the above call to parallelexecute, and keep in mind that the two thunks can be interleaved arbitrarily: • lookup value of x • add 10 to the value of x • set x to the result • lookup value of x • add 20 to the value of x • set x to the result If the operations were interleaved in the above manner (not interleaved at all), then the value of x at the end is 35. • lookup value of x • lookup value of x • add 10 to the value of x • add 20 to the value of x • set x to the result • set x to the result In the above interleaving, the value of x ends up being 15. This is not what we wanted! CS61A Summer 2010 – George Wang, Jonathan Kotker, Seshadri Mahalingam, Eric Tzeng, Steven Tang Notes adapted from Justin Chen, Chung Wu QUESTION: What are the possible values of x after the below? (define x 5) (parallelexecute (lambda () (set! x (* x 2))) (lambda () (if (even? x) (set! x (+ x 1)) (set! x (+ x 100))))) Concurrency: The Series We use something called “serializers” to make sure that certain chunks of code are executed together. First, we need a way to create a serializer: (define xprotector (makeserializer)) That was easy. A serializer takes in a procedure, and creates a serialized version of that procedure. So, (define protectedplus10 (xprotector (lambda () (set! x (+ x 10))))) (define protectedplus20 (xprotector (lambda () (set! x (+ x 20))))) protectedplus10 still does the same thing as the original thunk – take in no arguments, and add 10 to x. However, because protectedplus10 and protectedplus20 are created with the same serializer, their instructions will not be interleaved. Therefore, in doing, (parallelexecute protectedplus10 protectedplus20) you can always be sure that x will be set to 35 at the end. There’s also a primitive object called a “mutex” that’s even lower level than serializers (in fact, serializers are implemented with mutexes). You can interact with a mutex this way: (define m (makemutex)) (m ‘acquire) ;; “reserves” the mutex (m ‘release) ;; “releases” the mutex Once one program has acquired a mutex, if another wants to acquire the same mutex, it must wait until the mutex is released. So we can do this to obtain the same result: (define xmutex (makemutex)) (parallelexecute (lambda () (xmutex ‘acquire) (set! x (+ x 10)) (xmutex ‘release)) (lambda () (xmutex ‘acquire) (set! x (+ x 20)) (xmutex ‘release))) CS61A Summer 2010 – George Wang, Jonathan Kotker, Seshadri Mahalingam, Eric Tzeng, Steven Tang Notes adapted from Justin Chen, Chung Wu The calls to acquire and release a mutex marks the critical sections of the code – sections that should not be interleaved with other processes also needing the same mutex. When working with concurrency, there are four potential kinds of problems: 1. incorrectness – like the second interleaving example above, the answer you get might just be wrong 2. inefficiency – you could lock up the whole computer and always run only one program at a time, but that's horribly inefficient 3. deadlocks – if two programs are competing for the same two resources, there can be deadlocks 4. unfairness – one program may be unfairly favored to do more work than another QUESTION: The Dining Politicians Problem. Politicians like to congregate once in a while, eat and spew nonsense. One slow Saturday afternoon, three politicians meet to have such wild fun. They sit around a circular table; however, due to the federal deficit (funny that these notes are timeless), they are provided with only three chopsticks, each lying in between two people. A politician will be able to eat only when both chopsticks next to him are not being used. If he cannot eat, he will just spew nonsense. 1. Here is an attempt to simulate this behavior: (define (eattalk i) (define (loop) (cond ((caneat? i) (takechopsticks i) (eatawhile) (releasechopsticks i)) (else (spewnonsense))) (loop) (loop)) (parallelexecute (lambda () (eattalk 0)) (lambda () (eattalk 1)) (lambda () (eattalk 2))) ;; a list of chopstick status, #t if usable, #f if taken (define chopsticks ‘(#t #t #t)) ;; does person i have both chopsticks? (define (caneat? i) (and (listref chopsticks (rightchopstick i)) (listref chopsticks (leftchopstick i)))) ;; let person i take both chopsticks ;; assume (listset! ls i val) destructively sets the ith element of ;; ls to val (define (takechopsticks i) (listset! chopsticks (rightchopstick i) #f) (listset! chopsticks (leftchopstick i) #f)) ;; let person i release both chopsticks (define (releasechopsticks i) (listset! chopsticks (rightchopstick i) #t) (listset! chopsticks (leftchopstick i) #t)) ;; some helper procedures (define (leftchopstick i) (if (= i 2) 0 (+ i 1))) (define (rightchopstick i) i) Is this correct? If not, what kind of hazard does this create? CS61A Summer 2010 – George Wang, Jonathan Kotker, Seshadri Mahalingam, Eric Tzeng, Steven Tang Notes adapted from Justin Chen, Chung Wu 2. 3. Here's a proposed fix: (define protector (makeserializer)) (parallelexecute (protector (lambda () (eattalk 0))) (protector (lambda () (eattalk 1))) (protector (lambda () (eattalk 2)))) Does this work? Here’s another proposed fix: use one mutex per chopstick, and acquire both before doing anything: (define protectors (list (makemutex) (makemutex) (makemutex))) (define (eattalk i) (define (loop) ((listref protectors (rightchopstick i)) ‘acquire) ((listref protectors (leftchopstick i)) ‘acquire) (cond ... ;; as before) ((listref protectors (rightchopstick i)) ‘release) ((listref protectors (leftchopstick i)) ‘release) (loop)) (loop)) Does that work? 4. What about this: (define m (makemutex)) (define (eattalk i) (define (loop) (m ‘acquire) (cond ... ;; as before) (m ‘release) (loop)) (loop)) 5. So what would be a good solution? (Note: This problem is commonly referred to as “The Dining Philosophers” problem. However, here at Berkeley, we prefer to look down on politicians rather than philosophers.) CS61A Summer 2010 – George Wang, Jonathan Kotker, Seshadri Mahalingam, Eric Tzeng, Steven Tang Notes adapted from Justin Chen, Chung Wu Streaming Along A stream is an element and a “promise” to evaluate the rest of the stream. You’ve already seen multiple examples of this and its syntax in lecture and in the book, so I will not dwell on that. Suffice it to say, streams is one of the most mysterious topics in CS61A, but it’s also one of the coolest; mysterious, because defining a stream often seems like black magic (and requires MUCH more trust than whatever trust you worked up for recursion); cool, because things like infinite streams allows you to store an INFINITE amount of data in a FINITE amount of space/time! How is that possible? We’re not going to be too concerned with the below
the
line implementations of streams, but it’s good to have an intuition. Recall that the body of a lambda is NOT executed until it is called. For example, typing into STk: (define death (lambda () (/ 5 0))) Scheme says “okay”, happily binding death to the lambda. But if you try to run it: (death) ;; Scheme blows up The crucial thing to notice is that, when you type the define statement, Scheme did NOT try to evaluate (/ 5 0) – otherwise, it would’ve died right on the spot. Instead, the evaluation of (/ 5 0) is delayed until the actual procedure call. Similarly, if we want to represent an infinite amount of information, we don’t have to calculate all of it at once; instead, we can simply calculate ONE piece of information (the streamcar), and leave instructions on how to calculate the NEXT piece of information (the “promise” to evaluate the rest). It’s important to note, however, that Scheme doesn’t quite use plain lambda for streams. Instead, Scheme memorizes results of evaluating streams to maximize efficiency. This introduces some complications that we’ll visit later. The delay and force operators, therefore, are special operators with side effects. QUESTIONS 1. Define a procedure (ones) that, when run with no arguments, returns a cons pair whose car is 1, and whose cdr is a procedure that, when run, does the same thing. 2. Define a procedure (integersstarting n) that takes in a number n and, when run, returns a cons pair whose car is n, and whose cdr is a procedure that, when run with no arguments, does the same thing for n+1. Using Stream Operators Here are some that we’ll be using quite a bit: (streammap proc s ...) – works just like list map; can take in any number of streams (streamfilter proc s) – works just like list filter (streamappend s1 s2) – appends two finite streams together (why not infinite streams?) (interleave s1 s2) – interleave two streams into one, with alternating elements from s1 and s2 CS61A Summer 2010 – George Wang, Jonathan Kotker, Seshadri Mahalingam, Eric Tzeng, Steven Tang Notes adapted from Justin Chen, Chung Wu Constructing Streams This is the trickiest part of streams. I said that the topic of streams is a black art, and you'll soon see why. The construction of streams is counter
intuitive with a heavy dose of that
can’t
possibly
work. So here are some rough guidelines: 1. consstream is a special form! consstream will NOT evaluate its second argument (the streamcdr); obviously, this is desirable, since we’d like to delay that evaluation. 2. Trust the, err, stream. From the first day, we've been chanting “trust the recursion”. Well now that you're (slightly more) comfortable with that idea, we need you to do something harder. When you're defining a stream, you have to think as if that stream is already defined. It's often very difficult to trace through how a stream is evaluated as you streamcdr down it, so you have to work at the logical level. Therefore, the above definition of integers works. However, be careful that you don't trust the stream too much. For example, this won't work: (define integers integers) 3. Learn how to think about streammap. Consider this definition of integers, given the stream ones, a stream of ones, defined in SICP: (define integers (consstream 1 (streammap + ones integers))) If the above definition of integers puzzles you a bit, here's how to think about it: 1 1 2 3 4 5 6 ... + 1 1 1 1 1 1 ... =========================== 1 2 3 4 5 6 7 ... <= your streamcar <= integers (as taken from the last line) <= ones <= integers If you’re ever confounded by a streammap expression, write it out and all should be clear. For example, let’s try a harder one – partialsum, whose ith element is the sum of the first i integers. It is defined thus: (define partialsum (consstream 0 (streammap + partialsum integers))) 0 0 1 3 6 10 15 ... + 1 2 3 4 5 6 ... =========================== 0 1 3 6 10 15 20 ... <= your streamcar <= partialsum <= ones <= partialsum Now, if you find it odd to have integers or partialsum as one of the things you’re adding up, refer to guideline #2. 4. Specify the first element(s). Recall that a stream is one element and a promise to evaluate more. Well, often, you have to specify that one element so that there’s a starting point. Therefore, unsurprisingly, when you define streams, it often looks like (consstream [first element] [a stream of black magic]). But there are many traps in this. In general, what you’re avoiding is an infinite loop when you try to look at some element of a stream. streamcdr is usually the dangerous one here, as it may force evaluations that you want to delay. Note that Scheme stops evaluating a stream once it finds one element. So simply make sure that it’ll always find one element immediately. For example, consider this definition of fibs that produces a stream of Fibonacci numbers: (define fibs (consstream 0 (streammap + fibs (streamcdr fibs)))) CS61A Summer 2010 – George Wang, Jonathan Kotker, Seshadri Mahalingam, Eric Tzeng, Steven Tang Notes adapted from Justin Chen, Chung Wu Its intentions are admirable enough; to construct the next fib number, we add the current one to the previous one. But let’s take a look at how it logically stacks up: 0 <= your streamcar 0 1 1 2 3 5 ... <= fibs? + 1 1 2 3 5 8 ... <= (streamcdr fibs) =========================== 0 1 2 3 5 8 13 ... <= not quite fibs... Close, but no cigar (and by the definition of Fibonacci numbers you really can’t just start with a single number). Actually, it’s even worse than that; if you type in the above definition of fibs, and call (streamcdr fibs), you’ll send STk into a most unfortunate infinite loop. Why? Well, streamcdr forces the evaluation of (streammap + fibs (streamcdr fibs)). streammap is not a special form, so it’s going to evaluate both its arguments, fibs and (streamcdr fibs). What’s fibs? Well, fibs is a stream starting with 0, so that’s fine. What’s (streamcdr fibs)? Well, streamcdr forces the evaluation of (streammap + fibs (streamcdr fibs)). streammap is not a special form, so it’s going to evaluate both its arguments, fibs and (streamcdr fibs). What’s fibs? Well, fibs is a stream starting with 0, so that’s fine. What’s (streamcdr fibs)? You get the point. How do we stop that horrid infinite loop? Well, it was asking for (streamcdr fibs) that was giving us trouble – whenever we try to evaluate (streamcdr fibs), it goes into an infinite loop. Thus, why don’t we just specify the streamcdr? (define fibs (consstream 0 (consstream 1 (addstreams fibs (streamcdr fibs))))) So, then, let’s try it again. What’s (streamcdr fibs)? Well, (streamcdr fibs) is a stream starting with 1. There! Done! See? Now, it’s pretty magical that adding one more element fixes the streamcdr problem for the whole stream. Convince yourself of this. As a general rule of thumb, if in the body of your definition you use the streamcdr of what you’re defining, you probably need to specify two elements. Let’s check that it logically works out as well: 01 <= your streamcar 0 1 1 2 3 5 ... <= fibs + 1 1 2 3 5 8 ... <= (streamcdr fibs) ============================== 0 1 1 2 3 5 8 13 ... <= win! QUESTIONS: Describe what the following expressions define. 1. (define s1 (addstream (streammap (lambda (x) (* x 2)) s1) s1)) 2. (define s2 (consstream 1 (addstream (streammap (lambda (x) (* x 2)) s2) s2))) 3. (define s3 (consstream 1 (streamfilter (lambda (x) (not (= x 1))) s3))) CS61A Summer 2010 – George Wang, Jonathan Kotker, Seshadri Mahalingam, Eric Tzeng, Steven Tang Notes adapted from Justin Chen, Chung Wu 4. (define s4 (consstream 1 (consstream 2 (streamfilter (lambda (x) (not (= x 1))) s4)))) 5. (define s5 (consstream 1 (addstreams s5 integers))) 6. Define facts without defining any procedures; the stream should be a stream of 1!, 2!, 3!, 4!, etc. More specifically, it returns a stream with elements (1 2 6 24 ...) (define facts (consstream 7. (HARD!) Define powers; the stream should be 1 , 2 , 3 , …, or, (1 4 16 64 ...). Of course, you cannot use the exponents procedure. I’ve given you a start, but you don’t have to use it. (define powers (helper integers integers)) (define (helper s t) (consstream 1 2 3 Constructing Streams Through Procedures You’ll find this the most natural way to construct streams, since it mirrors recursion so much. For example, to use a trite example, (define (integersstarting n) (consstream n (integersstarting (+ n 1)))) So (integersstarting 1) is a stream whose first element is 1, with a promise to evaluate (integersstarting 2). The rules are similar to above; you still specify a first element, etc. Pretty simple? Let’s try a few. QUESTIONS 1. Define...
View
Full Document
 Fall '08
 Harvey
 Integers, the00, a00

Click to edit the document details