logoalt Hacker News

gavinray • today at 1:14 AM • 4 replies • view on HN

Languages have not yet evolved to multi-core properly IMO

I've sketched what I think an ideal "language of today" would look like, and one of the core tenets is making concurrent code explicit and part of the design surface.

You might imagine a webserver with main code like:

  spawn(on_core: 0) {
    await receive_packet(input)
  }
Or iterating over "num_cpus" and doing the same thing, with hashing based on CPU number for shared-memory resources.

Replies

xyzzy_plugh • today at 1:33 AM

The problem is that when you build at this layer, you end up writing far more scheduling code than business logic. Eventually you've built the abstractions we already use today.

The vast, vast majority of software, and software developers, simply do not care about core affinity, NUMA, noisy neighbors, etc.

Even on systems or situations where this matters (massive number of cores and memory, multiple sockets, thousands of threads, etc.) the juice is rarely worth the squeeze.

Doubly so when you end up being I/O bound anyways.

Nearly every software developers wants their software to work across multiple deployment targets. Maybe you just target x86-64, and you don't care about the number of CPUs because you'll be upgrading your servers every couple of years.

No one wants to deal with difficult-to-reproduce issues related to scheduling in niche configurations. It's much more efficient for runtime or compiler engineers to solve these problems for us, leveraging the vast amount of resources of corporations to develop and test these features. Then you have a place your code can live happily ignorant of its runtime constraints.

And that, my friend, is the beauty of our industry.

➕ show 1 reply
tancop • today at 7:30 AM

Even better with structured concurrency and no function coloring like Verse:

  count := AtomicU32.new(0)
 
  spawn {
    loop {
      input := try race {
        sock.receive()
        console.receive()
      }
      handle_packet(input)
      count += 1
    } match {
      Error e -> log.error(e, "main loop failed")
      nil -> log.info(fmt"Finished with {count} requests")
    }
  }
The receive functions are async so they can only be used inside an async context, like the one created by race. Race is a keyword from Verse that returns the result of the first finished task and cancels the other.
jmyeet • today at 2:51 AM

I disagree. In a world of different architectures, out-of-order execution, asymmetric cores, branch prediction, per-core caching and information leakage between threads and the branch not taken, my opinion is firmly that most people shouldn't be writing multithreaded code because it is almost impossible to get it correct and safe.

The effects can be subtle and lurk for years until a seemingly unrelated change exposes them. It starts with optimizations like instead of spawning a thread, you use a thread pool, possibly abstracted as some kind of executor service. Other code might use different thread pools to isolate them because each pool has its own limits on the number of threads. Some might grow but usually they won't.

Until one day you end up with thread starvation and a deadlock through a series of benign seeming optimizations. There are threads around but not in the right pools and the entire thing grinds to a halt.

Cooperative async/await on a single thread is the ideal solution for most use cases, particularly with complete environment teardown between requests. Yes, this is basically the PHP model.