← Back to context

Comment by mrkeen

1 year ago

Maybe Async Rust is bad, I haven't tried it!

But as for the arguments:

> But even in 1999 Dan says this about cooperative M:N models:

>> At one point, M:N was thought to be higher performance, but it's so complex that it's hard to get right, and most people are moving away from it.

It is higher performance. If you have M jobs and you can get N workers to work on them at the same time, you win!

It is also complex. So if you want the feature, let the smart people working on runtime figure it out, so that each team of application developers in every company doesn't invent their own way of doing it. If not in the runtime, then let library developers invent it, so there's at least some sharing of work. (Honestly I probably prefer the library situation, because things can improve over time, rather than stagnate.)

> Many operating systems have tried M:N scheduling models and all of them use 1:1 model today.

Nope! At the application level, M is jobs and N is threads. But at the OS level, M is threads and N is cores. Would I be exaggerating to say that doing M:N scheduling is the OS's primary purpose?

> but how come M:N model is used in Golang and Erlang - 2 languages known for their superior concurrency features?

These examples are "the rule", as opposed to "the exceptions that prove the rule".

> The Coloring Problem

I'm sick of the What Color Is Your Function argument. The red/blue split exists, and not just for asynchrony. Your language can either acknowledge the split or ignore it:

* A blocking function can call a non-blocking function, but not vice-versa.

* An auth'd function can call a non-auth'd function, but not vice-versa.

* An impure function can call a pure function, but not vice-versa.

* An allocating function can call a non-allocating function, but not vice-versa.

* A subclass can call into a superclass, but not vice-versa.

* A non-deterministic function can call a deterministic function, but not vice-versa.

* A exception-throwing function can call a non-exception-throwing function, but not vice-versa.

Even the dependency inversion principle works this way: it's a plea for concretions to call abstractions, and not the other way around!

Trying to remove the red/blue split will not work, and you'll only be pretending it doesn't exist.

The "solution" (if you can call it that) is simply for library writers to expose more blue code and less red code, where possible. If your language acknowledges that red and blue are different, then application developers have an easier time selecting blue library imports and rejecting red ones. Which is somewhat aligned with the article's title. But application developers can do whatever - red/blue, go nuts.

> Trying to remove the red/blue split will not work, and you'll only be pretending it doesn't exist.

Go managed to so it. What exactly would "you're only pretending it doesn't exist" mean in context of Goroutines?

  • This post discusses the issues:

    https://news.ycombinator.com/item?id=38821840

    It's not to say that Go is bad in this regard! It is just (always) doing the heavy lifting for you of abstracting over different colors of functions. This may have some performance or compatibility (especially wrt FFI) concerns.

    Rust chose not to do this, which approach is "right" is subjective and will likely be argued elsewhere in this thread.

    • I don't think anyone is suggesting that Go's concurrency model is perfect. However, the OP said "trying to remove the red/blue split will not work". This is a pretty strong claim, and Go seems like a reasonable counterexample to it.

      Similarly, if someone said "trying to marry async to a language with lifetime analysis and no GC will not work", it would be reasonable to point to Rust as a counterexample, even though Rust async has various problems.

      2 replies →

    • From the blog:

      > There are two key drawbacks to this otherwise interesting and useful decision. First, Go can't have exceptions. Second, Go does not have the ability to synchronize tasks in real (wall clock) time. Both of these drawbacks stem from Go's emphasis on coroutines.

      1) Go can't have exceptions? What exactly are panics, if not a peculiar implementation of exceptions? They print stack trace of the panicking goroutine, just like exceptions print stack traces of the thread they are thrown in. What exactly is the difference?

      2) For real-time workloads, you can pin goroutine to an OS thread and use a spinlock. How does this make it different than in any other language?

      > Since goroutine stacks are thus made disparate -- goroutines do not "share" common "ancestor" stack frames like Scheme's continuations do -- they can unwind their own stacks. However, this also means that when a goroutine is spawned, it has no memory of its parent, nor the parent for the child. This has already been noticed by other thinkers as a bad thing.

      Goroutines are made to resemble lightweight threads. Maybe the author considers threads bad, but that's just a subjective opinion. But-- at the end of the blog, there's a sentence:

      > OS threads provide some very nice constructs for programmers, and are hardened, battle-tested tools.

      Goroutines provide almost exactly the same semantics as OS threads, so I don't really get what they're trying to say.

      > Consider something of a converse scenario: Goroutine a spawns a goroutine b, without using an anonymous function this time. No closure, just a simple function spawn. Coroutine a opens a database connection. Goroutine b panics, crashing the program. The database connection is then left open as a zombie TCP connection.

      On any sane OS, when the program crashes, the kernel closes the TCP connection - there is no such thing as a "zombie" TCP connection.

      With all due respect to whoever the author is, I think this blogpost is full of crap.

    • UnixODBC in Go?? Zombie TCP connections from a crashing program. The author is clueless on these subjects. Not worth arguing over a misinformed blog post.

  • I don't know any Go so I'll try some pseudocode that hopefully maps across well enough.

      main {
        chan = makeChannel()
        sendMsg(chan, "one")
        sendMsg(chan, "two")
        print(recvMsg(chan))
        print(recvMsg(chan))
      }
    
      sendMsg(...) {
        async {
          // ...
        }
      }
    

    I argue that this code is all-red when sendMsg is allowed to spawn an extra (green)thread to do its work (at the async keyword.) The order of the prints in main is unknown. If you remove the async, the code becomes all-blue and the order of the prints becomes known.

  • Go managed to do many things ... with the help of a runtime, which the Rust team doesn't seem to be very fond of.