hedin_hiervard 2 hours ago

I was once asked a very similar problem during a job interview for a position of a software engineer. I was offered to solve it on a piece of paper or orally. We ran out of time and they offered me to take it home. I did and I spent a few hours thinking about it. Google and ChatGPT were not able to produce better results than the intuitive answer, so I built a test program that confirmed the weird behaviour. After meditating for a few hours I was able to find the correct answer and I was indeed fascinated by this. Do you also think it's too much for a usual interview?

  • kunley an hour ago

    Kudos for solving this. And yes, this is too much for a job interview.

colonCapitalDee 5 hours ago

It's never actually explained how n=2 is possible. Here's how it works:

P1 and P2 start executing. P1 sets temp=1, then P2 runs through the loop for i=1..9. Then P1 sets n=temp=1. P2 sets temp=2, then P1 resumes executing for i=2..10, and completes. P2 sets n=temp=2, then completes.

The intuition is P1 checkpoints, P2 runs through the loop up until the last iteration, P1 resets n to the checkpoint + 1, P2 checkpoints, P1 runs through the loop, then P2 resets n to the checkpoint + 1.

  • simplegeek 2 hours ago

    Thank you! I initially included the illustration and explanation in the first draft but decided to remove them in favor of the full trail. Looking back, I think that was a mistake. I’ve now added back the illustration along with some explanation.

fjfaase 3 days ago

This implicitely assumes atomic assignments, meaning that during an assignment all bits representing a value are transfered in one atomic unit. This sounds a bit trivial, but if one would be working with large integers that are stored in multiple memory words, it is less trivial.

I think it is possible to implement locking with only atomic assigments.

  • bcoates 10 hours ago

    Yeah, looking at the code my 'intuition' was "this is a classic data race, the program is broken" not "somewhere between 10 and 20".

    I suppose if you assume all global accesses are atomic, it's a good demonstration that atomic operations don’t compose atomically (even in trivial cases) and aren't particularly useful as concurrency primitives.

Groxx 9 hours ago

To be pedantic (which I feel is fair because it's a concurrency question about possibilities):

>If we run P and Q concurrently, with ‘n’ initialized to zero, what would the value of ‘n’ be when the two processes finish executing their statements?

It depends a lot on the observer of `n` and what relationship it has with P and Q. Which isn't defined.

E.g. a trivial boundary-answer is that it can be 0, if nothing guarantees that P's and Q's threads' memory reaches the `n`-observer's thread. This is somewhat common if you try to synchronize via `sleep`, because that usually doesn't guarantee anything as all.

We also don't know the computer architecture. We can probably assume at least one byte moves between memory levels atomically, because basically every practical system does that, but that doesn't have to be true. If bits move between memory levels independently, this could observe 31, because it can be any combination of the bits between `00000` and `10100`, which includes `01011` -> there can be a `1` in any position, including all positions, so you can observe a number that was never assigned. (IRL: multi-word values and partial initializations can do this, e.g. it's why double-checked locking is flawed without something to guarantee initialization completes before the pointer is exposed)

  • simplegeek 6 hours ago

    You're right, I could have phrased it better to something like:

    "If we run P and Q concurrently with ‘n’ initialized to zero, what extreme interleaving could result in the lowest value of 'n' when the two processes finish executing their statements on a model checker?"

    I'll edit it to improve, thanks.

    • Groxx 5 hours ago

      tbh I think zero is a completely reasonable result for a general thought-exercise like this. sleep-based "memory fixes" are quite common in practice, and there's no synchronization at all in the sample.

      though there's a lot of fun in here if you allow for optimizing compilers and lack of synchronization. e.g. without explicit synchronization between P/Q and the observing thread (assuming it's the main thread), it's reasonable for a compiler to simply delete P and Q entirely and replace the whole program with `print(0)`

tombert 9 hours ago

This was fun to port over to PlusCal to verify it myself:

    EXTENDS Naturals, Sequences
    
    (*--algorithm StateStore {
    
    variables
      n = 0; completions = <<>>; 
    
    define {
        Invar == Len(completions) = 2 => n # 2
    }
    
    fair process (thing \in {"p", "q"})
    variables i = 0, temp = 0; 
      {
       Loop:
            while (i < 10) {
               First:
                   temp := n + 1;
               Second:
                   n := temp;
               Last: 
                   i := i + 1; 
        };
        
        Complete:
            completions := Append(completions, 1)
      }
    }*)

(I acknowledge that this isn't the most elegant Pluscal but I think it is a roughly accurate?)
  • simplegeek 6 hours ago

    Thank you :) I was wondering how this would look in TLA+/PlusCal.

dtgriscom 9 hours ago

Seems pretty clear that the result could be anything from 1 to 20.

(Assumes atomic assignments, as noted by someone else.)

  • vbsd 6 hours ago

    What sort of an interleaving would produce 1? Seems provably impossible to me, assuming atomic assignments.

Izikiel43 9 hours ago

The intuition I developed over the years says that result is unknown, it's a race condition, all bets are off. Specially since doing N+1.