←back to thread

193 points ingve | 5 comments | | HN request time: 0.834s | source
Show context
ilrwbwrkhv ◴[] No.43712785[source]
Rust has a bunch of these while being maintainable.
replies(4): >>43713075 #>>43714151 #>>43714332 #>>43724210 #
ykonstant ◴[] No.43714332[source]
Isn't concurrency in Rust a notorious pain point? Or am I confusing it with async which is different? [I am stuck in an era before parallelism, so I don't really understand these things]
replies(2): >>43714755 #>>43715499 #
mrkeen ◴[] No.43714755[source]
This why I haven't fully embraced Rust yet. Whenever I ask about safely mutating shared state (see sibling comment), I'm met with silence, or some comment like: Rust guarantees that you aren't mutating shared state.
replies(1): >>43720236 #
1. sriram_malhar ◴[] No.43720236[source]
I'm surprised you say this. The core type system guarantees is that there is no aliasing while mutating, and no mutation while it is aliased. You get single writer multiple reader for free without overhead.

If you want multiple writers, you can always use the Arc container and use the built-in lock.

replies(1): >>43720412 #
2. mrkeen ◴[] No.43720412[source]
>> or some comment like: Rust guarantees that you aren't mutating shared state.

> The core type system guarantees that there is no [sharing] while mutating, and no mutation while [sharing]

replies(1): >>43725429 #
3. sriram_malhar ◴[] No.43725429[source]
Not sure what you are pointing out, so let me spell out what I said earlier.

1. You get single-writer multiple reader for free from the type system, without any runtime overhead.

2. For the same reason, the type system does not allow multiple writers. If you want multiple writers, then you are forced to use locks. Once you use locks, the runtime guarantees safety for this case.

Either way, you get 100% safety.

replies(1): >>43726546 #
4. mrkeen ◴[] No.43726546{3}[source]
You're not adding any new information. I already understand you completely.

> 1. You get single-writer multiple reader for free from the type system, without any runtime overhead.

Take the example from the article which is accepted by the Haskell type system:

   writeTBCQueue :: TBCQueue a -> a -> STM ()
   writeTBCQueue q v = do
     stillOpen <- readTVar q.open
     when stillOpen $ writeTBQueue q.queue v
Rust would reject this because of multiple writers.

Also, thinking about it more, I'm now very skeptical of Rust even providing 'single-writer multiple-reader'. Is it in fact single-reader-writer xor multiple-reader? In other words, how does it handle a goblin constantly moving money between accounts while a gnome is constantly trying to count the total amount?

  goblinBankerThread = forever $ do
    seed <- randomSeed
    atomically $ do
        (acctA, acctB) <- chooseTwoRandomAccounts seed
        if (amount acctA) > (amount acctB)
            then moveAmount $5 acctA acctB
            else moveAmount $5 acctB acctA

  gnomeAccountantThread = forever $
    atomically $ do
      accounts <- readAllAccounts
      assert someConstantAmount allAccounts
Yes, Rust is 100% safe because it would reject this code, so it would never run. Not running code also has guaranteed no-overhead!

2. For the same reason, the type system does not allow multiple writers. If you want multiple writers, then you are forced to use locks

* Locks are problematic, which is why I chose STM over locks in the first place.

* Locks are in all the languages. Does your comment about 100% safety really apply to all languages ?

replies(1): >>43729643 #
5. sriram_malhar ◴[] No.43729643{4}[source]
Your first point is not comparing the same thing. STM is wonderful, but as you no doubt know, it is meant for many TVars to be read/modified. This necessarily has overhead (transactional logs), performs poorly under contention and also is subject to livelock, and has no fairness.

In your goblin example, I believe the gnomeAccountantThread would have to constantly retry, because the writer (if successful) would have produced a new version of two accounts, which would trip up the reader, forcing it to start again. In general, Haskell's STM is built for short-lived transactions; for longer running transactions or those that touch a lot of objects, you'd need something like multi-versioned objects seen in databases or epochs to get a consistent snapshot. Neither Rust nor Haskell is suited to this example out of the box.

For your second question, you assume axiomatically that locks are problematic. They aren't in Rust (except, see later about deadlocks). Unlike any other language with in-place mutation, Rust will force you to use a mutex in order to share something for read-write (in a multiple writer scenario), otherwise it won't compile. You have to use lock() in order to get access to the underlying object, and once you have that object, the type system makes sure only the owner can mutate it. In C/C++/Java/Go, you don't get this guarantee at all ... it is possible to mistakenly use the object without using a mutex. So, there is not guarantee of safety in the other languages. There is a 100% guarantee in Rust.

---

That said, the problematic part about locks (whether it is mutexes or MVars in Haskell) is deadlocks, which is solved by having a deterministic lock order. In your Haskell example, if acctA and acctB were MVars, you'd do

    let (first, second) = if acctA < acctB then (acctA, acctB) else (acctB, acctA)
       withMVar first  $ \_ ->
         withMVar second $ \_ -> do
            ...