Hacker News new | past | comments | ask | show | jobs | submit login
How does the concurrency primitives in Rust compare to the ones in Go?
43 points by samuell on June 5, 2014 | hide | past | web | favorite | 19 comments
From what I read about the concurrency primitives in Rust, they seem to be more inspired by the actor model in languages such as Erlang (and Scala?) than Golangs "hardwired" channels.

Is this notion correct?

At the same time it seems it has the counterparts of go-routines (lightweight tasks) and I have seen channels mentioned.

All in all, I would welcome a little elaboration on what are the differences between the concurrency primitives in the two languages.




I cannot speak for Rust, but I can speak for Go. The actor model is almost the reverse of how Go handles concurrency. Instead of sending to named actors, in Golang you name your pipes (or channels). Then you handle these pipes as first class citizens and routines can choose to receive or send on them. e.g.

1. In your main routine you build a channel of ints (noticed they're typed) named `x`

2. You hand `x` and an integer i (incremented by one for each new routine) to ten routines which will double that number and send the result down `x`

3. Your main routine then receives on `x` and builds a list of results. It hasn't blocked on the doubling operations for each integer `i` because you sent each of those operations in a new routine.

Ignoring the fact I haven't managed closing the channel `x` you can get the gist of how channels are named and passed around.

I can't say what the alternative would be in Rust, but I hope that sheds more light on Go!


This is similar to Rust:

    let (tx, rx): (Sender<int>, Receiver<int>) = channel();
    
    spawn(proc() {
        let result = some_expensive_computation();
        tx.send(result);
    });

    some_other_expensive_computation();
    let result = rx.recv();
There are some subtle details I'm not 100% sure of (bounded vs. unbounded?) that may be different, though.

For more: http://doc.rust-lang.org/guide-tasks.html


This is completely the opposite: in Rust you have an explicit sender and an explicit receiver, and you have no control on the channel that was created. Only this sender can send to the pipe, and only this receiver can receive.

In go, all you have is the channel. Anyone can send to it/read from it:

    c := make(chan int)

    go func() {
        // I can write here
        c <- 1
    }()

    go func() {
         // I can also write here
         c <- 2
    }()

    // I can read here, even though I have no idea who wrote to the channel
    for a := <- c {
        // Note that since I don't know who wrote to c, I can't expect 
        // any order here. All I can do is process stuff that is coming
        // down the pipe (which is all I really care about after all
    }


> This is completely the opposite: in Rust you have an explicit sender and an explicit receiver, and you have no control on the channel that was created. Only this sender can send to the pipe, and only this receiver can receive.

Its not "completely the opposite", though it is different. Rust doesn't let multiple tasks use the same sender handle, but supports multiple senders on the same channel by cloning the sender handle (same with the receiver handle, mutatis mutandis.)

So your Go code becomes something like this in Rust (the only substantive difference is the need to clone the sender handle):

  let (tx, rx) = channel();

  spawn(proc() { 
    tx.send(1); 
  });
  
  tx2 = tx.clone();
  spawn(proc() { 
    tx2.send(2); 
  });
  
  for a in rx.iter() {
    // As in the Go example, process the stuff coming down
    // the channel with no expected order or knowledge of
    // who sent to it. 
  }


Does anyone know how this compares to what's newly available in the standard C++11 libraries?


Right so you name the sender and receiver right? Then say a channel exists between them? Then send and receive are explicitly called on the sender and receiver respectively. Which is the opposite of how Go names the channel and the `sender` is the routine which calls a send `x<-` on the named channel. The `receiver` in Go would be the main routine which calls receive on the named channel `<-x`. I think that is the subtle difference.


> Right so you name the sender and receiver right?

No, you create a channel with a sender and receiver handle, and then the tasks that are going to send or receive use those handles. You can think of send and receive handles as loosely analogous to send- or receive-only channels in Go (they are slighty different because only one task can use each handle, but they are cloneable to allow multiple tasks to use the channel, it just forces multiplexing on either end to be more of an explicit choice.)


You are correct.

(1) Rust disallows shared-memory concurrency (with a few exceptions), so each "task" has it's local heap. Only unique objects can be transferred from one task to another. This is similar to Erlang.

(2) They were using light-weight processes initially, but then transitioned to a 1-on-1 threading model, where each task runs on its own thread. IIRC it was to reduce the runtime library (lightweight threads require stack-growth handling and a user-mode scheduler).


Rust actually supports both 1:1 and M:N threading models, depending on which one you want. It's true that it used to be M:N by default and is now 1:1 by default, though.

> Rust disallows shared-memory concurrency (with a few exceptions),

To elaborate: Rust eliminates mutable state being passed across a concurrency boundary at compile time. Sometimes, you really need shared mutable state, though, so you're able to use 'unsafe' to implement the safety the compiler can't infer for you. The obvious ones (only allowing access via a mutex and reference counting (both atomic and non)) are already in the standard library.


To be clear, unsafe code is not required to use shared state (even shared mutable state with atomics/locks). The unsafe code is encapsulated in the standard library and the types are set up such that it enforces safe, data-race-free usage of the primitives.


Right, the unsafe is an implementation detail. I guess I still consider that 'using' unsafe even if you didn't write it yourself.


That's interesting with the M:N threading model. But would that require that I set the M/N ratio (or "multiplexing factor" or what to call it) to a fixed value, unlike the ability in Go to automatically multiplex any number of go-routines on the (fixed) number of OS threads?


I'm not intimately familiar with Go's details here, but you can either just say 'run these green threads kthx' in which that factor is chosen for you, or you can use an explicit pool(s) and do it yourself.

Details: http://doc.rust-lang.org/green/index.html


libgreen will automatically pick a reasonable default number of OS threads to use if you don't, based on the number of CPU cores. (This is different from the way GOMAXPROCS works.) You may spawn any number of tasks and they will be automatically multiplexed onto those threads.


Go's threading model is M:N threading. N is the fixed number of kernel threads, M is the variable number of application threads.


Go's core philosophy for concurrency is: Do not communicate by sharing memory; instead, share memory by communicating [1]. I have not tried to share memory (ex: a pointer-to-struct) between two different go threads before, so I can't really comment on what happens when one tries to break out of the language's philosophy. However, I've found that following the philosophy is incredibly liberating as it encourages a certain kind of reasoning that gives me the confidence I can accurately statically analyze the code without having to debug it at run time. Like any other philosophy though, it isn't for everyone.

EDIT: I now realize I never addressed your concern of primitives. Go really only has one: the channel. It can send anything (even other channels). The two types are buffered or unbuffered, and deciding which to use can alter the behavior of a program.

There are a few language keywords to support the use of channels, such as `for-select-case` loops and `go` to spawn new goroutines. The former is used to help respond to numerous channels, the latter to spawn more goroutines. Nothing in the language prevents listening to a runtime-determined arbitrary number of channels, either.

[1] http://blog.golang.org/share-memory-by-communicating


> I have not tried to share memory (ex: a pointer-to-struct) between two different go threads before, so I can't really comment on what happens when one tries to break out of the language's philosophy.

As in other languages, you have to handle it--either through locking or some other means.


Sorry, I should have clarified though that the Rust part is the new territory for me, while I'm pretty familiar with the Go primitives already. Thanks for the clarification though!


Rust channels seem more like Go's channels than Erlang's actor mailboxes -- in Rust like Go and unlike Erlang, a task can receive from multiple channels, not just the one that it owns as a result of being an actor.

The difference is that while channels support multiplexing at both ends in Rust, just as they do in Go, that multiplexing requires explicit cloning of the handles in Rust, whereas it is the default behavior in Go. So, Rust channels are like Go channels but where multiplexing is explicit rather than implicit.




Applications are open for YC Summer 2019

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: