6 ms·
There may be some huge piece I'm missing, but how exactly is the "starting multiple async tasks wrapped in blocks and waiting for them to finish at the end of t
by Toutouxc 5y ago
There may be some huge piece I'm missing, but how exactly is the "starting multiple async tasks wrapped in blocks and waiting for them to finish at the end of the main Async block" different from "starting multiple threads wrapped in blocks and manually collecting them at some point"? I thought Ruby did release the GIL when a thread is blocked (waiting for IO etc).
- rajangdavis 5y agoIt looks like from a brief glimpse into the source code that the library is using Ruby Fiber's under the hood instead of Threads. From my limited understanding, the programmer has to be explicit about starting and resuming the work within a Fiber as opposed to the Ruby VM.
- brunosutic 5y agoYes, async is using fibers as a concurrency primitive. Async gem is starting fibers automatically. Fiber pausing and resuming is handled by event reactor (also called "event loop") from 'nio4r' gem. io_uring support with asynchronous File IO is also on the way.
- rajangdavis 5y agoAwesome! Thanks for the article, can't wait to see this library progress!
- Toutouxc 5y agoSo the Async gem allows the programmer to fire "tasks" and wait for them to finish (the same way we can fire threads), but instead of OS-level threads (which is what MRI uses for Threads) it uses a new kind of Fibers, called "non-blocking Fibers", that are lightweight and don't use OS threads, like normal Fibers, but unlike normal Fibers they yield automatically to the scheduler when blocked (sort of like threads). Is this a correct-ish way to describe the current state of affairs?
- brunosutic 5y agoI think you're generally on the right track. Small correction: fibers (blocking or non-blocking) never used threads in any way. Fibers are "stackful coroutines", another concurrency primitive. They're pretty hard to understand, and unfortunately I can't find a good explanation of "Ruby Fibers" that I can link to.
- brunosutic 5y agoRuby's Global Interpreter Lock (GIL) is applicable, and not "sidestepped" with Async.
- brunosutic 5y ago- Async Ruby is much more performant than threads. There are less context switches, enabled by the event reactor. The performance benefits are visible in simple scenarios like making a thousand HTTP requests. - Async is more scalable, can handle millions concurrent tasks (like HTTP connections). There can only be a couple thousand threads at the same time. - Threads are really hard to work with - race conditions everywhere. Async doesn't have this.
- Toutouxc 5y ago> race conditions everywhere. Async doesn't have this What does Async (Fibers underneath) do differently than normal Threads? Using threads to handle concurrent work doesn't immediately bring race conditions, unless the programmer explicitly creates them (accessing the same stuff from different threads). Fibers themselves AFAIK don't stop you from accessing the same stuff, aside from the obvious "the fiber code runs 'atomically' until it hits the next yield (which non-blocking Fibers take away anyway).
- BiteCode_dev 5y agoI assumee unless you have a GIL, most operations on collections are not thread safe, but they are async safe, since context switching can only happen where you await.
- gpderetta 5y agoThat's true for stackless coroutines where you can only yield from the top, but I understand that ruby has full coroutines/fibers
- brunosutic 5y ago> What does Async (Fibers underneath) do differently than normal Threads? There's a lot to say on this topic. - Threads implement "preemptive scheduling". A scheduler switches control from one thread to another every 10ms or so. A thread running the code may be ready for the switch or not. The ensuing race conditions are nasty. - Async + Fibers implement "cooperative scheduling". The currently running fiber (voluntarily) yields control to another fiber when it's ready. The result is there are no race conditions. There's so much to say about this, I'll blog about this in the future.
- sizediterable 5y agoYeah, it's unclear to me whether you'd have to "join" to get the result from an operation, or if you can have some form of "await" expression
- Toutouxc 5y agoApparently the tasks are "joined" automatically before the enclosing "Async do" block is allowed to finish.
- Ginden 5y agoThreads use quite much more memory than coroutines. Spawning eg. 3 threads for each request, x 1000 requests per second would probably eat a ton of memory.
- liuliu 5y agoStackful coroutines (often used to implement colorless async) is the same except you can specify stack size. You can specify thread stack size yourselves as well, though no one does that. OTOH, growable stack is useful, as Go demonstrated.
- Smaug123 5y agoPlease, someone, correct me if I've misunderstood. The big difference appears to be that async Ruby does not merely give you an easy sugar to perform the sync-over-async antipattern you have described. The real innovation is that, as far as the user is concerned, Ruby is magically turning blocking methods into non-blocking ones.
- ioquatix 5y agoThat's pretty accurate.
- nixpulvis 5y agoThat's basically how I'm thinking of things as well. To illustrate a bit further, consider the following: Given a blocking method call `foo(x)`, I can make it non-blocking by wrapping it in a "thunk" as `λx.foo(x)`. Where things start to get interesting is when I add another method call `foo(x) + bar(x)`. Now to keep things "async" I need to transform the abstraction into something more like `λx.foo(x) + λx.bar(x)`, and have the `+` call dispatch both fibers and wait for them before performing its operation. Doing this automatically seems pretty cool, I'll have to think about this a bit more sometime.
- burlesona 5y agoThe difference is just that fiber overhead is lower so you can run more fibers than threads on a given system. Even though fibers have been around a while, people rarely used them because they are cooperative rather than preemptive, so you had to manually write the scheduling logic. Much easier to just use threads. I think the big breakthrough for Ruby Async is that fiber scheduler in Ruby 3.0 now makes it possible for the runtime to manage fibers in a less manual way, so you now get the lightweight option more easily. The Async gem seems to be wrapping all that up in a very nice interface so you can write simple code and get good concurrency without much effort.
- DarkWiiPlayer 5y agoThe difference only shows itself in the real world, when you do a bit more per thread/coroutine and end up mutating shared state. This is where threads can lead to race conditions, whereas coroutines will not (unless you're basically ask for it)