Live data from Hacker News

Local async executors and why they should be the default

maciej.codes

271–280 of 327 posts

Re: Local async executors and why they should be the default

#271

Earlier quoted context omitted.

He isn't suggesting that. He's suggesting it is the right place to start , in the same way that we normally start writing sync code with a single thread.

Nope. Direct quote: > “..and when you need to utilize multiple CPU cores, you just spawn multiple processes that listen on the same socket. This is a much better way of structuring servers..”

That's not a contradiction. He's saying that later when you need multithreading you can add it. You generally don't start with it.

Of course for some projects you know up front you'll need multithreading but the point is that that isn't the default position.

Re: Local async executors and why they should be the default

#272

Earlier quoted context omitted.

Yes. The state of the art technology has not percolated everywhere. Google's tech stays locked up in Google. Parallelism in many languages that are mainstream is difficult to get right and error prone. I don't think most developers should be working with low level threads or synchronization primitives for business purposes. I am working on a notation and runtime and I'm thinking of automatic parallelisation. There is…

Google is not the 70s, and they haven't really done much, and not recently. Google's big thing in that area maybe was MapReduce, which was quite behind the state of the art when they first introduced it. I believe they eventually moved to BSP, which is more in line with what academia have been developing since the early 90s, but that's not really proprietary stuff. But in any case, if you're just talking about schedu…

I am not familiar with any particular research, I just don't doubt that what I'm doing has not been done before. I still think it's worth doing.

What I meant was that some ideas get trapped in corporations and never open sourced. If you have any papers you recommend, I would read them. I have a list of whitepapers on my Github profile.

Recently I was trying to paralellise the A* graph search algorithm for code generation. I got it parallel by sharding neighbour discovery per thread. This speeds it up, so it scales because I sharded the problem space.

What if people could model the problems as data flow problems and then the paralellisation is automatic? In my experience all the tutorials and materials on the internet refer to threads or go channels. These are low level primitives for what I'm aiming for.

Thanks for the reference to "Bulk synchronous parallel", I had not heard of that.

I'm investigating at turning arbitrary OOP style code into LMAX Disruptor style pipelined actor code that crunches through events in parallel.

My goal is that programs written in my notation are parallel by default, without explicit design due to sharding and scheduling. I tried naively tried to shard a simple bank without my syntax and that gets 700 million requests per second on 12 threads, because I shard the money into different integers across threads. I want this kind of thinking to come by default.

The automatic parallelisation I was thinking about I've seen is to do with loops or autovectorisation. I want to combine event streams with loop parallelisation. I am also interested in SIMD+multithreading together but I've not studied that in any detail.

I am inspired by Alan's Kay's original idea of OOP programming.

So I've turned objects that are routed to "actors" or "tasks" that emit "event arrays" or streams and then parallelise their processing. Loops are first class citizens. This is inspired by coroutines and generators.

I turn control flow to data and parallelise that in addition to data parallelism.

I use routing to shard.

Take the canonical example of being a search giant and you want to download multiple files, parse them for links, then index those links and then save them somewhere. You have CPU heavy tasks and IO heavy tasks. I want to multiplex IO with coroutines per thread and CPU tasks per multiple threads. You could say this is an example of what you describe as scheduling graphs of tasks.

  url(url) | task download-url
  for url in urls:
    fire document(url, download(url))

  document(url) | task extract-links
  parsed = parse(document)
   fire parsed-document(url, parsed)

  parsed-document(parsed) | task fetch-links
  for link in document.query("a")
    fire new-link(url, document, link)


  new-link(url, document, link) | task save-data
   fire saved-link(url, link, db.save(url, link))

  for url in ["http://samsquire.com/", "https://devops-pipeline.com/"]:
   fire url(url)
This program corresponds to the following event stream:

  url("https://samsquire.com/")
  url("https://devops-pipeline.com/")
  document("https://samsquire.com/", downloaded_document)
  document("https://devops-pipeline.com/", downloaded_document)
  parsed-document("https://samsquire.com/", parsed)
  parsed-document("https://devops-pipeline.com/", parsed)
  new-link(url, document, link)
  new-link(url, document, link)
  saved-link(url, link, db_callback)
  saved-link(url, link, db_callback)
url() events are in array, document() are in an array() parsed-document() are in array, so they can be performantly processed, by different threads compiled down to loops.

They can be routed to shard.

The events of this event stream can be dispatched from/in different threads (and machines) and routed to be crunched in parallel. Each event corresponds to a "mailbox" which corresponds to a thread mapping and we can define topologies of graphs like you say based on these objects and events.

Some of my thoughts:

* I am trying to combine coroutines with threads for efficient scheduling.

* If OOP interactions can be mapped to multidimensional array buffers, we can even inline code to be autovectorised as simple loops.

* I want to integrate my 1:M:N lightweight thread scheduler, and epoll based server (hopefully I can rewrite it to use liburing) with a general purpose parallelising runtime.

* Vitess is a sharding database proxy, sharding is extremely powerful paralellisation technique.

Re: Local async executors and why they should be the default

#273

Earlier quoted context omitted.

Nope. Direct quote: > “..and when you need to utilize multiple CPU cores, you just spawn multiple processes that listen on the same socket. This is a much better way of structuring servers..”

That's not a contradiction. He's saying that later when you need multithreading you can add it. You generally don't start with it. Of course for some projects you know up front you'll need multithreading but the point is that that isn't the default position.

Yes it is. Spinning up another process is not multithreading, like, literally. It’s multiprocess. I never cease to be amazed with the complete lack of basic CS literacy from JS script kiddies.

Re: Local async executors and why they should be the default

#274
post #135

Wait, async is multithreaded by default in Rust? For me the whole point of using async in JavaScript or Python (originally with Twisted's @inlineCallbacks) was to get concurrency without threads. Imagine writing code for a computer game bot: move left, wait for enemy, attack enemy... You normally can't write it like this because it would block the rest of your program. Async allows you to go from "program sequential"…

The point being made isn't whether or not the futures are evaluated on one thread or many, but that by default, library authors assume that something may be evaluated on multiple threads which imposes some constraints on both the argument and return types from functions. So for example, the tokio executor could run single threaded or on a thread pool. However, tokio::task::spawn takes a future as an argument that may…

That's just what I was trying to say. If your language potentially allows futures to resume on different threads, you pay a complexity cost. I'm not too familiar with Rust, but I think the cost is especially high here because Rust is so concerned with correctness (there is no Send trait in C++ for example).

And if you are using async only for nicer control flow, then there is no need for multithreaded executors. Just coroutines that live on a single thread and get orchestrated by an event loop (trampoline/reactor/executor).

Re: Local async executors and why they should be the default

#275
post #113

Earlier quoted context omitted.

I really think in the end, in another decade or two, the community consensus is going to be that async as it is conceived of today is simply a mistake, full stop. Think about it. Where did it come from in its current incarnation? Node. Why did Node choose it? Did it have a multiplicity of options and carefully choose the best one based on years of experience with each choice? No. Async was "chosen" because it was the…

We wrote asynchronous driven network code for decades without syntactic sugar for it, and it was fine. That 5% performance gain can be had without it. Async syntax is a mistake. Not just because of the mess it makes across the program tree, but also because it brings with it a specific notion of how to do asynchronous I/O. io_uring for example gives an entirely different model, one with some lovely performance benefi…

> io_uring for example gives an entirely different model, one with some lovely performance benefits.

The io_uring model is also fundamentally async? So not sure what you’re arguing here. It’s just completion, rather than readiness based, but otherwise no different, and completely compatible with how other async things work in the Rust ecosystem (see tokio-uring).

If anything, I’d argue that io_uring is the best example of the benefits of async API’s over the “just spawn another blocking thread bro” model.

Re: Local async executors and why they should be the default

#276

Earlier quoted context omitted.

Any kind of mechanism like that requires a mental model that carries a ton of state because it is no longer immediately visible what the scope of execution is. Continuations and the way Erlang handles this have far less mental overhead and help keeping the model and the mental representation of that model in sync. Differences between the two is where bugs will hide. The web isn't asynchronous it's synchronous in almo…

Oh, hey again. :) And yes I work mostly with Elixir for years and Erlang's model is just irreplaceable so far. You're quite right that everything is synchronous but all the Erlang "processes" (green threads, fibers or w/e people want to call them) are preemptively and forcibly switched. And that has basically eliminated 95% of all parallel programming problems. I would kill for Erlang's concurrency / parallel primiti…

Keep an eye on gleam lang if you’re not already. It’s a language with an ML inspired type system (like rust) that compiles to erlang. It is likely too nascent to be used in production (in terms of tooling, ecosystem, stability, etc).

https://gleam.run

Re: Local async executors and why they should be the default

#277

Earlier quoted context omitted.

If you want golang in Rust just use channels and tasks? Or threads? I don't find async Rust difficult at all, I'm having a hard time really empathizing with this to the extent of needing breaking changes. To me, async from a lang perspective is virtually done - in 2024 I suspect all of the various impl Trait and async Trait stuff will be done and at that point I don't see anything left.

I do that of course, and that's one of the easiest ways to use async Rust. In real projects you need much more however. F.ex. I had to code an example of how to add tasks to an already running pool of tasks and posted my findings here: https://github.com/dimitarvp/rust-async-examples/blob/main/e... (there's #2 as well with some more comments and a different approach). The fact that I needed to make a GitHub repo and…

> The fact that I needed to make a GitHub repo and start making show-and-tell demos on how to do various things

While I resonate with this, because I also went through a period of struggling with async stuff, I genuinely think this is because async is just hard in general. Done properly, it yields an immense amount of power, quite efficiently, but also opens up a lot of “degrees of freedom” about it can be operated, which leads to confusion.

A lot of the async stuff, how it actually worked, and how to actually use it, only clicked for me when I played around with Glommio, which runs an executor-per-core, and some of the constraints it imposed made understanding it all somewhat easier.

Re: Local async executors and why they should be the default

#278

Earlier quoted context omitted.

I do that of course, and that's one of the easiest ways to use async Rust. In real projects you need much more however. F.ex. I had to code an example of how to add tasks to an already running pool of tasks and posted my findings here: https://github.com/dimitarvp/rust-async-examples/blob/main/e... (there's #2 as well with some more comments and a different approach). The fact that I needed to make a GitHub repo and…

> The fact that I needed to make a GitHub repo and start making show-and-tell demos on how to do various things While I resonate with this, because I also went through a period of struggling with async stuff, I genuinely think this is because async is just hard in general. Done properly, it yields an immense amount of power, quite efficiently, but also opens up a lot of “degrees of freedom” about it can be operated,…

Thanks a lot of the Glommio mention, that's an instant star and I'll review it in more details Soon™.

> Done properly, it yields an immense amount of power, quite efficiently, but also opens up a lot of “degrees of freedom” about it can be operated, which leads to confusion.

Yeah, very well put. Indeed it's very powerful and sure it's confusing. I need guard rails. And I need my hands slapped much more. "Looks like you're trying X -- this is how you do it, you idiot" would work well. :D

I am almost not joking even.

Re: Local async executors and why they should be the default

#279

Earlier quoted context omitted.

Oh, hey again. :) And yes I work mostly with Elixir for years and Erlang's model is just irreplaceable so far. You're quite right that everything is synchronous but all the Erlang "processes" (green threads, fibers or w/e people want to call them) are preemptively and forcibly switched. And that has basically eliminated 95% of all parallel programming problems. I would kill for Erlang's concurrency / parallel primiti…

Keep an eye on gleam lang if you’re not already. It’s a language with an ML inspired type system (like rust) that compiles to erlang. It is likely too nascent to be used in production (in terms of tooling, ecosystem, stability, etc). https://gleam.run

I do already keep an eye on it and I like its syntax a lot. Problem is that the current commercial Elixir ecosystem is very strongly gravitating towards web and API development where several libraries reign supreme (Phoenix [web framework] and many of its dependents and derivatives, plus Absinthe [GraphQL] and Ecto [databases]). They also heavily rely on Elixir's macros so Gleam has quite a lot of work to do before it gains any tangible switching-over power.

To be honest... I am more likely to learn Golang more deeply (I know it quite well already but haven't, like, programmed in it in production for a long time, I am mostly using it for my own scripting and personal project needs) or even dive into OCaml now that it has a multithreaded runtime.

I do like how enthusiastically people make new languages but IMO most of them should be absorbed back into the hivemind at one point. This huge fragmentation does not help anything (except maybe teach you a technique or two which is of course very valuable by itself).

Re: Local async executors and why they should be the default

#280
post #135

Earlier quoted context omitted.

The point being made isn't whether or not the futures are evaluated on one thread or many, but that by default, library authors assume that something may be evaluated on multiple threads which imposes some constraints on both the argument and return types from functions. So for example, the tokio executor could run single threaded or on a thread pool. However, tokio::task::spawn takes a future as an argument that may…

That's just what I was trying to say. If your language potentially allows futures to resume on different threads, you pay a complexity cost. I'm not too familiar with Rust, but I think the cost is especially high here because Rust is so concerned with correctness (there is no Send trait in C++ for example). And if you are using async only for nicer control flow, then there is no need for multithreaded executors. Just…

I think you're still missing it. Rust futures can be evaluated on one thread. They don't need to be Send either. It's that library authors need to add the constraints to their APIs because they may be multithreaded.
Post reply on HN