LMU ☀️ CMSI 6998
CONCURRENT AND DISTRIBUTED COMPUTING
Final Exam Answers
  1. Here is a goroutine that accepts a number $n$ and a wait group, and prints the $n$th fibonacci number then announces its completion to the wait group before exiting. Note that in Go, a goroutine is just a function. It would also be acceptable to wrap the function in a go statement, but it was not necessary.
  2. func fibonacci(n int, wg *sync.WaitGroup) {
        defer wg.Done()
        if n <= 0 {
            fmt.Println(0)
            return
        }
        a, b := 0, 1
        for i := 2; i <= n; i++ {
            a, b = b, a+b
        }
        fmt.Println(b)
    }
    
  3. A Java function (method) that returns the maximum value in an array by delegating the job of finding the max of each array half to two platform threads:
    public static double max(double[] a) {
        if (a == null || a.length == 0) {
            throw new IllegalArgumentException("Empty array has no maximum");
        }
        double[] maxes = new double[2];
        var t1 = Thread.ofPlatform().start(() -> {
            maxes[0] = Arrays.stream(a, 0, a.length / 2).max().getAsDouble();
        });
        var t2 = Thread.ofPlatform().start(() -> {
            maxes[1] = Arrays.stream(a, a.length / 2, a.length).max().getAsDouble();
        });
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Internal Thread interruption", e);
        }
        return Math.max(maxes[0], maxes[1]);
    }
    
  4. Java CountDownLatches cannot be reused.
  5. Asynchronous code is nonblocking.
  6. Synchronous primitives are a superior choice for a language's concurrency model because (1) synchronous code is easier to read, understand, predict, reason about, and maintain; (2) it is more efficient in terms of resource usage as it avoids the overhead of buffers and callbacks; and (3) it is considerably easier to implement async code on top of synchronous primitives than the other way around! Pretty much any of these answers are acceptable.
  7. addpd is an x86 SIMD instruction.
  8. A CRCW PRAM would solve the first-index-containing-a-value problem in $\Theta(1)$ time if the instruction to write the result gave priority to the lowest numbered process in a concurrent write.
  9. The 5-node hypercube has $5(2^{5-1}) = 5 \cdot 16 = 80$ connections.
  10. A parallel algorithm is cost-efficient if $\frac{W^*(n)}{C(n)} \in \Theta((\log n)^k)$ for some constant $k$. In other words, the cost has to be within a polylogarithmic factor of the optimal cost.
  11. The Go equivalent of the given Ada conditional entry call is:
    select {
    case e <- struct{}{}:
        fmt.Println("Win")
    default:
        fmt.Println("Ima cry")
    }
    
    This is because the Ada entry call was parameterless, so the right thing to do is send the empty struct to a channel.
  12. In Ada, when you create a task via new, you block until the new tasks finishes activating. Given this fact, none of the other options apply, as they are all in conflict with this reality.
  13. There is no terminate statement in Ada. There is a terminate alternative in a select statement, which is close, but certainly not a statement. The incorrect code you are looking at shows terminate placed in a statement context, which is not allowed.
  14. Coroutines are associated with cooperative multitasking.
  15. Mailboxes should use protected objects because they only receive messages, but relays should use tasks because they both send and receive messages. Sends are active.
  16. P and H are duals.
  17. ”I probably will have taken the exam” is ”It is possible that: (There will be a time in the future that: (At some time before it: (Exam x is taken by me)))”. In Temporal Logic: $\lozenge\textbf{F}\textbf{P}(Tmx)$.
  18. The only one that works is:
    function f() {
      return computeTheThing().then(x => process(x));
    }
    
    Others were close but had subtle issues, like the ones missing a return statement and the one that called the function instead of passing the function itself to the then method.
  19. The ONLY possible order of operations is:
    1. 1. The value 1 is printed.
    2. 2. An event to log 2 is placed on the FIFO event queue, to be executed after 0ms have passed.
    3. 3. An event to log 3 is placed on the FIFO event queue, to be executed after 0ms have passed.
    4. 4. The value 4 is printed.
    5. 5. The event to log 2 is executed, printing 2. The other event cannot get in first, because the queue is FIFO.
    6. 6. The event to log 3 is executed, printing 3.
  20. In a web server, most clients are I/O bound, not compute bound, so virtual threads are a good fit.
  21. The code sample should make you think about the possibility of deadlock if a thread tries to get a lock that it already holds. This why reentrant locks were created. The fact that deadlock might occur shows the degree of responsibility required under explicit locking. The problem really had nothing to do with semaphores (locking twice has nothing to do with multiple threads), barriers (no one is synchronizing) nor condition variables (as this problem illustrates a resource issue).