[HN Gopher] Async Ruby
___________________________________________________________________
Async Ruby
Author : brunosutic
Score : 187 points
Date : 2021-10-30 16:11 UTC (6 hours ago)
(HTM) web link (brunosutic.com)
(TXT) w3m dump (brunosutic.com)
| rememberlenny wrote:
| This seems huge.
|
| Does anyone know plans around how this will be implemented into
| Rails?
| Mikeb85 wrote:
| It's not particularly huge, it's just another gem. Rails
| already does things concurrently and in parallel.
|
| https://guides.rubyonrails.org/threading_and_code_execution....
| joelbluminator wrote:
| It does it with threads though, which has implication when we
| talk about moderate/big traffic and Action Cable. A big win
| would be if Action Cable will be able to work with
| Fibers/Async or something equivalent. It's an annoying
| performance bottleneck and it would be super awesome if
| Ruby/Rails just solves it.
| adverbly wrote:
| This is all new to me as well, but the project mentioned the
| Falcon web server(https://github.com/socketry/falcon).
|
| The documentation for Falcon mentions using it with rails:
| https://socketry.github.io/falcon/guides/rails-integration/i...
|
| I imagine something more "native" to rails will happen
| eventually though. But would need to be after this makes its
| way into core ruby(which has not happened yet apparently).
| hiyer wrote:
| From reading the article it seems to me that you could just
| wrap any Rails method (say a DB read) in an Async block to make
| it run asynchronously.
| rajangdavis wrote:
| Also says at the end:
|
| The only caveat is that it doesn't work with Ruby on Rails,
| because ActiveRecord doesn't support async gem. You can still
| use it with Rails if ActiveRecord is not involved.
| hiyer wrote:
| Oh, sorry I missed that. Hopefully it will show up in Rails
| 7 then.
| rajangdavis wrote:
| You're not wrong. If you have external services that are
| accessible via HTTP, there is some potential to get some
| performance gains there. You're just limited in terms of
| some of the core Rails utilities.
| DangitBobby wrote:
| I don't understand. Is this misleading then? Did HTTParty
| have to implement Async for their example to work?
|
| > You probably have your preferred HTTP gem, and you may be
| asking "will it work with Async"? To find out, here's an
| example using HTTParty, a well-known HTTP client.
|
| They then go to show that your "preferred HTTP gem" will
| just work. How is the ActiveRecord situation different?
| ioquatix wrote:
| ActiveRecord does work but it's hugely limited because
| they have explicit per-thread resource pools which we
| can't get around very easily.
| DangitBobby wrote:
| Okay, that makes sense.
| rajangdavis wrote:
| I think there has to be massive benefit to the Rails community
| before there can be a consideration of adding such a feature.
|
| I could be wrong here, but I'm not sure it will solve some of
| the more drastic issues of what happens in a production Rails
| app which I believe to be mostly around memory and garbage
| collection (from my limited experience and understanding).
|
| You _might_ be able to eke out more performance in terms of
| having more clients be able to hit a page, but I suspect that
| might make memory more of an issue, not less.
| joelbluminator wrote:
| How about ActionCable though? That's a big one I'm wondering
| about with Fibers/Async
| rajangdavis wrote:
| No clue... I could be wrong, but the underlying technology
| for ActionCable is Web Sockets and I'm not sure what
| problems async would solve there.
| joelbluminator wrote:
| I'm guessing the Rails implementation for the webs
| sockets part is thread-based and if that's the case
| fibers can make a big difference. But I'm also a bit out
| of my depth here.
| kovacs wrote:
| I think what's being talked about here is the back end
| implementation for ActionCable. By default it uses ruby
| threads to push over open web sockets. There's at least
| one production quality drop in implementation
| (https://anycable.io/) that address the default
| scalability issues you'll have with ActionCable. The
| async support would seem to allow one to go much further
| with default rails before needing to move to something
| more performant.
| brunosutic wrote:
| Async has async-websocket gem. I don't have experience with
| it so can't tell how it compares to ActionCable.
|
| https://github.com/socketry/async-websocket
| brunosutic wrote:
| Yea, it's a big new Ruby feature.
|
| From what I was able to tell (I followed the discussion around
| this) enabling Rails to work with Async is not huge amount of
| effort. One guy was working on this, but he got distracted with
| other stuff.
|
| It's just a matter of time someone steps up and gets this to
| work.
| BMorearty wrote:
| This gem had a huge impact in May of this year on one of Airbnb's
| Ruby services. Last year when I was still at Airbnb I did the
| research and foundational work to make Airbnb's HTTP client gem
| compatible with the Async gem and Falcon server. I'm no longer
| there but this year, one of my friends who's still at Airbnb put
| that work to use on Airbnb's Ruby service that talks to a Genesys
| API.
|
| Most Genesys API calls have reasonable response times but there
| are these occasional, weird, unpredictable 30-second response
| times. The slow responses can come in bursts, where you can a
| bunch of them at once. This makes it extremely hard to
| predictably scale the Ruby service that calls it, because in
| normal synchronous mode, every thread that calls Genesys is
| blocked and on longer available to take more client requests. You
| could scale up the service to assume these 30-second response
| times will happen all the time, but then you've got huge server
| costs just for the 1% case.
|
| But using Async and Falcon (an Async-compatible HTTP server),
| Airbnb was able to safely scale down the service by around 85%
| and turn on autoscaling, and it's working _beautifully_.
| faebi wrote:
| How will this deal with temporary CPU starvation in long running
| connections? I am currently using concurrent-ruby's thread pool
| to run a few hundred Net-SSH Connections per process at the same
| time. This helped me to avoid most of these kind of issues.
| sizediterable wrote:
| > any blocking operation (a method where Ruby interpreter waits)
| is compatible with Async and will work asynchronously within
| Async code block with Ruby 3.0 and later.
|
| That's pretty magical. Does this mean it would be possible to
| implement structured concurrency [0], but without the function
| coloring problem?
|
| Regardless, I think I prefer how this looks from a code
| read/writeability perspective compared to Zig's or Swift's (and
| potentially future Rust's [2]) approaches.
|
| [0] https://vorpus.org/blog/notes-on-structured-concurrency-
| or-g...
|
| [1] https://kristoff.it/blog/zig-colorblind-async-await/
|
| [2] https://blog.yoshuawuyts.com/async-overloading/
| azth wrote:
| The first reference is the same route that Java is taking with
| its green thread implementation by means of Project Loom[0].
|
| [0]
| https://cr.openjdk.java.net/~rpressler/loom/loom/sol1_part2....
| DangitBobby wrote:
| As someone who operates mainly in Python, I am so jealous. As
| far as I'm aware [1], you have to re-write your tooling to take
| advantage of async in Python. Does anyone have any insight into
| why Python async doesn't work the same way? Does it come down
| to fundamental language differences?
|
| 1. https://stackoverflow.com/a/63179518/4728007
| sizediterable wrote:
| I'm optimistic that this will be less painful as more of the
| Python ecosystem becomes async-friendly. We have `aiohttp` as
| a suitable replacement for `requests`, and major libraries
| like Django (not ORM yet), Flask, FastAPI, SQLAlchemy now
| have async support as well.
| DangitBobby wrote:
| It has already gotten less painful as time goes on.
| However, in my experience so far if I need e.g. Django to
| use Google Cloud Storage for media storage, I need to write
| my own shims that marry together an async alternative to
| Google's Python SDK (which I have no idea if there are any
| plans for async support) with third part django packages
| that I'd normally use. The experience isn't terrible, but I
| end up spending a lot of time trying to making consistent
| calling conventions.
|
| And I think I've come to the conclusion that any re-usable
| code I write for IO should just be written async with magic
| sync wrappers for use in sync contexts.
| brunosutic wrote:
| > Does it come down to fundamental language differences?
|
| No, I don't think there's anything fundamentally different.
|
| Ruby 3.0 implements a "fiber scheduler" feature that enables
| "colorless Async". Fiber scheduler is an obscure Ruby
| feature, but the end result is brilliant. It was also a huge
| amount of work.
|
| Side note: Fiber scheduler was implemented by the same guy
| who created Async Ruby - Samuel Williams. This guy is the
| mastermind (and master-coder) behind this project.
| pansa2 wrote:
| > _Does anyone have any insight into why Python async doesn
| 't work the same way?_
|
| Ruby async seems to be implemented using stackful coroutines.
| IIRC Guido has been opposed to adding these to core Python,
| preferring stack-less coroutines because they require every
| yield-point to be explicit (i.e. marked with `await`).
|
| There are libraries for a python that support stackful
| coroutines, such as gevent.
| brunosutic wrote:
| Async Ruby is colorless!
|
| It's obvious from the examples provided in the post. For
| example, you can use the method `URI.open` both synchronously
| and asynchronously.
|
| It's something I didn't want to mention in the article because
| it's a relatively advanced async concept, but yea - Async Ruby
| is already colorless and it's great.
| gpderetta wrote:
| IIRC ruby has first class continuations, so it doesn't have
| to deal with any async/await nonsense.
| brunosutic wrote:
| That's right - Async Ruby uses fibers (stackful coroutines)
| as a concurrency primitive. There's a lot to say about
| this, but the end result is that we get to write simple
| synchronous code and it can run asynchronously.
| nixpulvis wrote:
| On a tangential note.
|
| > Async Ruby is colorless!
|
| I find it deeply irritating that this is how we choose to
| describe things these days. Rather than talking about
| modality, or even using a few more words to describe the
| actual differences in terms of the underlying programing
| language models.
|
| Telling me that Ruby's async is colorless implies that I know
| about the description of programming models as "colors" of
| code, which was just some random analogy someone came up with
| which caught on. Nothing about this has anything to do with
| color. Even syntax highlighting doesn't really fit. This is
| akin to particle physicists getting cheeky and calling one of
| the quantum states "color" when it has nothing to do with
| wavelength (correct me if I'm wrong).
|
| This is an appeal to stop trying to be so cute. Thank you.
| bradgessler wrote:
| What is "colorless" suppose to mean? This is the first time
| I've seen it used.
| vmarquet wrote:
| I think it's a reference to this blog post
| https://journal.stuffwithstuff.com/2015/02/01/what-color-
| is-...
| jsnell wrote:
| Context:
| https://journal.stuffwithstuff.com/2015/02/01/what-color-
| is-...
| veesahni wrote:
| Does Falcon + Async allow a second request to be processed while
| the first is blocked on a network call?
|
| For example:
|
| * HTTP request 1 requires information for which we can't respond
| immediately.. so we hold the connection open and respond in a few
| seconds.
|
| * Can HTTP request 2 come in at the same time?
|
| EventMachine enabled the above with what they called "streaming
| responses"
| Matthias247 wrote:
| That seems mostly like the question of "Does this HTTP
| framework support HTTP pipelining". While I don't know the
| answer, it doesn't seem highly relevant. Most clients went away
| from using pipelining, since follow-up requests on the same
| connection are subject to unknown latency (stuck behind the
| first request) and a connection failure can impact all of those
| requests.
|
| The better approach is to use either more connections, or
| proper request multiplexing via HTTP/2 or /3. In the latter
| case a server framework would just see multiple request
| invocations in parallel.
| brunosutic wrote:
| Yes, this is possible!
|
| In fact this is a perfect use case for Falcon + Async.
| veesahni wrote:
| I just tried this out. Falcon with count=1 & sinatra. Worked
| perfectly. It seems every request is processed in an async
| block so literally no special code is required. A request
| waiting on network will allow others to go through.
|
| This is awesome! :)
| Toutouxc wrote:
| 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 wrote:
| It 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 wrote:
| Yes, 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 wrote:
| Awesome! Thanks for the article, can't wait to see this
| library progress!
| Toutouxc wrote:
| So 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 wrote:
| I 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 wrote:
| Ruby's Global Interpreter Lock (GIL) is applicable, and not
| "sidestepped" with Async.
| Ginden wrote:
| Threads 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 wrote:
| Stackful 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.
| sizediterable wrote:
| Yeah, 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 wrote:
| Apparently the tasks are "joined" automatically before the
| enclosing "Async do" block is allowed to finish.
| Smaug123 wrote:
| Please, 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 wrote:
| That's pretty accurate.
| nixpulvis wrote:
| That'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 `lx.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
| `lx.foo(x) + lx.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.
| brunosutic wrote:
| - 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.
| YorickPeterse wrote:
| > There can only be a couple thousand threads at the same
| time.
|
| You can easily have tens of thousands of threads on Linux.
| Beyond 50 000 or so you may need to adjust some settings
| using `sysctl`, but after that you should be able to push
| things much further.
|
| Task counts themselves are also a pretty useless metric.
| Sure, you can have may fibers doing nothing. But once they
| start doing something, that may no longer be the case (this
| of course depends on the workload).
|
| > Threads are really hard to work with - race conditions
| everywhere. Async doesn't have this.
|
| You can still have race conditions in async code, as race
| conditions aren't limited to just parallel operations.
| brunosutic wrote:
| > Task counts themselves are also a pretty useless metric.
| Sure, you can have may fibers doing nothing.
|
| Sorry if I wasn't explicit. I was talking about
| tasks/fibers performing actual work, like handling HTTP
| connections.
|
| It's practically possible for Async Ruby programs to do
| work with hundreds of thousands Async Tasks (concurrent
| fibers).
|
| Some users have worked with millions of Async tasks, but
| I'm not sure if it was practical work, or proof of concept.
|
| > You can easily have tens of thousands of threads on
| Linux.
|
| Thank you for the correction about threads on Linux. I'm
| not sure if you're talking about threads _in general_ or
| threads _in Ruby_?
|
| I've only lightly tested increasing the number of threads
| in Ruby to about a thousand, and my test code was _very
| slow_.
|
| I think that has to do with _thread switching overhead_.
| Threads have a relatively high switching overhead, so I don
| 't think it's advisable to run more than 100 threads - _in
| Ruby_ at least.
| formerly_proven wrote:
| > I think that has to do with thread switching overhead.
| Threads have a relatively high switching overhead, so I
| don't think it's advisable to run more than 100 threads -
| in Ruby at least.
|
| If you run many threads the bytecode VM of all threads
| are contending for the GIL in order to advance the
| program.
| Yoric wrote:
| > - Threads are really hard to work with - race conditions
| everywhere. Async doesn't have this.
|
| Having worked a lot with various flavours of async (I was one
| of the many people in the loop for the design of DOM Promise
| and JavaScript async), I regret that, while many developers
| believe it, *this is generally false*.
|
| In languages with a GIL or run-to-completion semantics, of
| course, you get _some degree of_ atomicity, which is a nice
| property. However, regardless of the language, once you have
| async, you have race conditions and reentrancy issues, often
| without the benefit of standard tools (e.g. Mutex, RwLock) to
| solve them [1].
|
| Ruby's async syntax and semantics look neat, and I'm happy to
| see this feature, but as far as I can tell from the examples
| in the OP, they're going to have these exact same issues.
|
| [1] Rust is kind of an exception, as its type system already
| forces you to either &mut/Mutex/RwLock/... anything that
| could suffer from data race conditions (or mark stuff as
| unsafe), even in async code. But even that is because of the
| race conditions and reentrancy issues mentioned above.
| zamalek wrote:
| > I regret that, while many developers believe it, _this is
| generally false_.
|
| The strongest evidence of a golden hammer is a lot of red
| thumbs.
| karottenreibe wrote:
| Can you please make or link to one example? I'd be
| interested to learn more.
| Yoric wrote:
| Just wrote one here:
| https://news.ycombinator.com/item?id=29051604
| gpderetta wrote:
| Exactly. At some point you realize that explicit callbacks,
| async, coroutines, threads are all related.
| brunosutic wrote:
| > I regret that this is generally false. Once you have
| async, you have race conditions and reentrancy issues
|
| Can you get more specific please?
|
| My experience is 100% from Ruby where I've worked heavily
| with threads in the past, and with Async Ruby for the past
| 18 months.
|
| From what I can tell, threads require a great deal of locks
| (mutexes) and are frustratingly hard to get right - because
| of the language-level race conditions.
|
| Async Ruby has been refreshingly easy to write. I have yet
| to encounter an example of a race condition.
|
| If it helps to bridge the language gap here: from what I
| know Async Ruby is similar to Go's goroutine model.
| lamontcg wrote:
| Here's a working race condition with async:
|
| https://gist.github.com/39409838626b22433f8bf1878276b3c1
|
| Prints 1 instead of 1000
|
| If you remove the sleep there's no race because the fiber
| scheduler never hits a blocking call anywhere, but the
| sleep stands in for some kind of i/o. And then there's
| the terrible global variable.
|
| (Conversely this does show you how badly you have to
| write code to get race conditions with async and the
| sleep there is important to the generation of the race,
| which would not be necessary with threads)
|
| And I think if you remove the sleep it doesn't race due
| to the GIL and single-threadedness of ruby for pure
| userspace computations?
| ricardobeat wrote:
| If it did _not_ print 1 then you 'd have a problem.
|
| Not really a race condition, just the semantics of async.
| You'll get the same with goroutines, javascript
| async/await, and other constructs.
| trinovantes wrote:
| You avoid race conditions of writing to the same variable
| but you still can't avoid race conditions where the
| critical section is longer than 1 statement
|
| e.g. in JS: foo = await fetch()
| bar = await fetch() doSomething(foo, bar)
|
| You can't be certain foo isn't modified while the second
| fetch is executing (assuming foo is globally scoped)
| wonnage wrote:
| There's still an important difference in that you're
| yielding control explicitly by using await, instead of
| being preemptable everywhere. That's good enough in most
| cases.
| Glyptodon wrote:
| In any language, avoiding race conditions or unexpected
| values changes without dedicated primitives or special
| conditions is relatively contingent on preventing
| thoughtless non-read usage of "shared" memory.
|
| The problem is that it's often relatively easy to make
| unsafe writes without realizing, especially since the
| guarantees of "safe" primitives can be misunderstood. And
| of course many people don't realize that async code can
| have race conditions because they don't really understand
| the details of how async even works, or that some
| languages make extra guarantees that they're unknowingly
| depending on.
|
| Having candidates explain the difference between parallel
| and asynchronous has been a relatively effective first
| level interview screener for me, especially with more
| junior roles.
| Yoric wrote:
| Here's a short toy example in pseudo-syntax.
|
| Task 1:
|
| global.a = 1
|
| await async {} // Await a block that does nothing,
| essentially yielding back to the reactor.
|
| print(global.a) // If you're unlucky, global.a has
| changed.
|
| Task 2:
|
| global.a = 2
|
| await async {} // Await a block that does nothing,
| essentially yielding back to the reactor.
|
| print(global.a)
|
| Now enqueue both tasks.
|
| Depending on scheduling, you could end up printing (1,
| 1), (1, 2), (2, 1) or (2, 2). This can become much worse
| if you're awaiting in the middle of filling a data
| structure.
|
| Feel free to replace `await async {}` with querying your
| database or a remote API and `global.a` with any variable
| or data structure that can be shared between the tasks.
|
| This example is, of course, a toy example, but I've had
| to debug sophisticated code that broke non-
| deterministically because of such behaviors. The source
| code of the front-end of Firefox (written in JavaScript)
| is full of hand-rolled kinda-Mutex implementations to
| avoid these race conditions.
| brunosutic wrote:
| Thank you for the clarification. You are right, these
| types of race conditions are possible with Async Ruby.
|
| I find these races relatively easy to spot: yes, global
| state CAN change when you yield back to the reactor.
|
| IMO thread race conditions are much, much worse. Global
| state can change AT ANY POINT, because thread scheduler
| preemptively switches threads. Here's an example:
| global.a = 0 thread { global.a = 1
| print(global.a) // a is 1 or 2 }
| thread { global.a = 2 print(global.a)
| // a is 1 or 2 } print(global.a) //
| a is 0, 1 or 2
| Yoric wrote:
| > I find these races relatively easy to spot: yes, global
| state CAN change when you yield back to the reactor.
|
| It's good that you can find them easily. In my
| experience, these changes can creep into your code
| stealthily and only hit you from behind, months later,
| when the winds shift.
|
| Some of my traumas include:
|
| - global state that ends up captured by a closure while
| you didn't realize it was (perhaps because the code you
| wrote and the code the other dev wrote accidentally
| shared state);
|
| - mutating hashtables/dictionaries while walking them,
| without anything in your code looking suspicious;
|
| - and of course accidentally mixing concurrency between
| two reactors, but luckily, that's not something that
| happens every day.
|
| > IMO thread race conditions are much, much worse.
|
| IMO, thread race conditions are a case of "it's bad but
| you knew what you'd get when you signed up for it", while
| async race conditions are a case of "don't worry,
| everything is going to be fine (terms and conditions
| apply, please contact your lawyer if you need
| clarifications)".
| bitwalker wrote:
| This isn't really an issue with threads though, the exact
| same issue is present in green thread/fiber
| implementations; it just so happens that in Async Ruby
| the GIL saves you from this specific problem due to
| making variable accesses atomic (as I understand it
| anyway, I'm not super familiar with the Ruby VM).
|
| In general, green threads/fibers are vulnerable to the
| exact same shared memory issues as threads, the only
| benefit to them is that they are, for a certain class of
| problems, a more efficient concurrency primitive than
| threads by avoiding context switches out of userspace,
| and in many cases provide you the ability to plug in your
| own scheduler if you so desire allowing you to optimize
| scheduling for your own workload.
| brunosutic wrote:
| Thank you for sharing your perspective in a non-
| conflicting way Yoric.
|
| It seems you have more experience than me with async-
| related work. I'll keep an eye on this kind of scenario
| you brought up.
| pansa2 wrote:
| > _Async Ruby is similar to Go 's goroutine model._
|
| Goroutines are just (lightweight) threads - they can run
| in parallel. You need locks whenever you access shared
| data.
|
| AFAIK Ruby's fibers are cooperatively scheduled and can
| only yield at function calls. Code that doesn't make a
| function call (e.g. incrementing an integer) is safe to
| run on multiple fibers without locks.
|
| For comparison, Python's stackless coroutines can safely
| do anything except `await` without requiring locks.
| Toutouxc wrote:
| > 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 wrote:
| I 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 wrote:
| That's true for stackless coroutines where you can only
| yield from the top, but I understand that ruby has full
| coroutines/fibers
| brunosutic wrote:
| > 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.
| Matthias247 wrote:
| Is the scheduler only using a single OS thread? It's not
| M:N scheduling?
| gpderetta wrote:
| Cooperative scheduling means that every function call is
| potentially a scheduling point, so it isn't really a
| significant change from threads. Sure, if you are careful
| and only call functions that are not guaranteed to
| preempt then you are safe, but critical sections make
| this explicit.
|
| And if you are running code on multiple cores then lack
| of preemption doesn't help anyway.
| dwaite wrote:
| > The currently running fiber (voluntarily) yields
| control to another fiber when it's ready. The result is
| there are no race conditions.
|
| This is generally untrue, as it assumes the developer
| knows the ramification of which methods hit scheduling
| points and understand what state the global set of
| potentially pending work might modify while suspended.
|
| You might not have to focus so much about concurrent
| threads modifying the same memory simultaneously, but you
| absolutely could have the value change unpredictably
| during a write-sleep-read.
|
| Note there are environments which try to make it more
| obvious which methods might result in a suspension, and
| have the developer acknowledge that so that the code
| remains understandable/maintainable. The keywords used
| for this are typically 'async' and 'await'.
| jakear wrote:
| Yep... this is why the "thank goodness these functions
| aren't colored!" people confuse me. Colored functions are
| a _very good thing_ , they make it explicit where context
| switches happen and make understanding async interactions
| _easy_.
|
| People who don't like "colored functions" IMO are similar
| to folks who don't like types. They want to be able to
| change something to be async without a compiler yelling
| at them to go through all of its call stacks and ensure
| they can handle the asynchrosity, similar to changing a
| function to return "null" sometimes and not wanting a
| compiler to make them verify all calling code can handle
| a null.
|
| That being said, I do wish all functions could be
| colored. The most painful async migrations are when
| calling code happens in a constructor, which in JS cannot
| be made async.
| pqdbr wrote:
| I have the same question as parent and I'd love to read
| more about this distinction. Please do blog about it.
| matheusmoreira wrote:
| > you can think of it as "threads with none of the downsides"
|
| And likely no actual parallel execution of Ruby code. I suppose
| fibers are scheduled for later when they perform I/O. Just like
| how C extensions release the interpreter lock before some
| expensive function, allowing other Ruby code to run in
| concurrently.
| dragonwriter wrote:
| > And likely no actual parallel execution of Ruby code.
|
| Async mentions "Multi-thread/process containers for
| parallelism."
|
| https://socketry.github.io/async/index.html
|
| These seem to be provided by a companion project:
|
| https://socketry.github.io/async-container/
| matheusmoreira wrote:
| > Multi-thread/process
|
| I thought so... Threads and processes are still in use. While
| yielding control on system calls is a great idea, it's a bit
| innacurate to say it's just like threads with none of the
| downsides. The upsides are missing too. Only the system calls
| are running in parallel here. The Ruby code remains single-
| threaded.
|
| Still, much better than languages that lack schedulers where
| people are tricked into writing cooperatively scheduled code
| without even realizing it.
| claudiug wrote:
| another cool ruby library for async is
| https://github.com/digital-fabric/polyphony
| brunosutic wrote:
| Polyphony monkey-patches Ruby core methods... this has always
| prevented me from experimenting more with it.
|
| Source link: https://digital-
| fabric.github.io/polyphony/faq/#why-does-pol...
|
| > Polyphony "patches" some Ruby core and stdlib APIs, providing
| behavioraly compatible fiber-aware implementations
| ksec wrote:
| I think this deserves more attention, especially it is coming
| from the original author of sequel.
|
| >Polyphony is a library for writing highly concurrent Ruby
| apps. Polyphony harnesses Ruby fibers and a powerful io_uring-
| based I/O runtime to provide a solid foundation for building
| high-performance concurrent Ruby apps.
|
| https://noteflakes.com
| jasonhansel wrote:
| No function colors! Is this like Go, where each fiber maintains a
| separate stack at runtime, or is this like Rust, where each task
| is effectively transformed into a state machine?
| jmarchello wrote:
| I can't wait to play with this! Having worked with Python's
| Asyncio a lot, this looks SO MUCH easier to work with.
| continuational wrote:
| That seems kinda verbose to use. It's also a bit suspicious that
| all the examples discard the results; is it hard to get the
| results back out?
| brunosutic wrote:
| > is it hard to get the results back out
|
| It's trivially easy to get the results, here's a quick example:
| require "async" require "open-uri" results
| = [] Async do |task| task.async do
| results << URI.open("https://httpbin.org/delay/1.6")
| end task.async do results <<
| URI.open("https://httpbin.org/delay/1.6") end
| end
| Spivak wrote:
| I'm not sure if this is the most realistic example since
| you're implicitly relying on this being at the top level and
| there being a global await at the end of the block. Surely
| any real program will have all the work done inside a single
| top-level event loop. require 'async'
| Async do results = [] Async do
| sleep 1 results << "Hello" end
| puts results # => [] end
| brunosutic wrote:
| I just ran your example and I'm getting `puts results` line
| to output "Hello", just as the program intends. I'm not
| sure why you're getting a different result.
|
| In any case, I'm assuring you: getting the results "out of
| tasks" is trivially easy.
|
| > Surely any real program will have all the work done
| inside a single top-level event loop.
|
| I don't get this. Can you please explain more what you have
| in mind and I'll try to help clarify things.
| Spivak wrote:
| Sorry, I needed to add a sleep to get the behavior I
| wanted. Now it just prints nothing.
|
| What I mean is I assume any real program is not going to
| be creating and destroying event loops any time they want
| to do something Async and that they'll essentially run
| main in a top level Async do. In fact it seems like
| that's the only safe thing to do with this library
| because the following snippet changes semantics depending
| on whether it's nested in an existing event loop or not.
| results = [] Async do sleep 10
| results << "Hello" end puts results
|
| So it seems like you'll pretty much always have to have
| an explicit wait before you can get your results in 99%
| of cases.
| brunosutic wrote:
| Wrap the example in a `Sync` block to get deterministic
| results. Now you don't have to worry if the code gets
| nested in an existing event loop. Sync
| do results = [] Async do
| sleep 10 results << "Hello" end
| puts results end
| ioquatix wrote:
| Every task is a promise you can wait on for the result.
| Concurrent fan out is trivial.
| Spivak wrote:
| Just tried it out. Async blocks evaluate to Task objects which
| have a wait method and a result attribute which evaluates to
| the value of the block. require 'async'
| res = Async do |task| name_task = task.async do
| sleep 2 "Jenny" end task.async
| do sleep 5 9 end
| "Hello #{name_task.wait}" end puts res # =>
| "Hello Jenny" after 5 seconds.
|
| After using Python's and JS's async implementations this seems
| beautiful by comparison. Here's the a rough Python equivalent.
| import asyncio async def get_name():
| await asyncio.sleep(2) return "Jenny"
| async def get_number(): await asyncio.sleep(5)
| return 7 async def main():
| number_task = get_number() name_task = get_name()
| name, _ = await asyncio.gather(name_task, number_task)
| return f"Hello {name}"
| print(asyncio.run(main()))
| derefr wrote:
| Also, it looks like there's a getter for the "current" task
| (https://socketry.github.io/async/guides/getting-
| started/inde...), sort of like Thread.current.
|
| Which means that you can just write a whole normal Ruby
| program, that just uses Task::Async.current.async(...)
| wherever it likes to schedule subtasks (sort of like calling
| spawn/3 in Erlang), and then treat them as regular futures,
| even returning the future out of the current lexical scope
| without thunking it; and then have exactly one Async block at
| the toplevel that kicks off your main driver logic and then
| #wait s on it. All _without_ having to pass the current task
| down the call stack everywhere.
|
| (And if you want to schedule a bunch of stuff to happen in
| parallel and then wait for it all to be done, but you're
| below the toplevel Async block, you'd do that by scheduling
| the subtasks against an Async::Barrier:
| https://socketry.github.io/async/guides/getting-
| started/inde...)
| brunosutic wrote:
| > Which means that you can just write a whole normal Ruby
| program, that just uses Task::Async.current.async(...) >
| All without having to pass the current task down the call
| stack everywhere.
|
| Yes, you can also use `Async { work }` instead of
| `Async::Task.current.async { work }`.
| Mikeb85 wrote:
| This gem is almost a decade old (although looks like it's been
| updated to work with fibers?). There's a bunch of gems that allow
| for asynchronous/concurrent and/or parallel execution for Ruby.
| It looks like a nice enough gem but it's not particularly novel.
| Seems like it's just trying to ride JS' popularity with the name.
| Ruby threads also allow for non-blocking operations.
|
| Edit - this isn't some new paradigm for Ruby. It's a gem... A
| bunch of others ("Parallel", "Concurrent-Ruby", "Eventmachine",
| etc...) have been around forever and do similar things.
|
| Edit2 - https://rubygems.org/gems/async
| brunosutic wrote:
| > This gem is almost a decade old
|
| Hm, the first commit to the gem was in 2017. The gem was not
| "advertised" so that the interfaces can be polished and done
| right.
|
| Async gem is _hugely_ improved with Ruby 3.0 release (from
| December 2020) when Ruby language added "fiber scheduler"
| feature just to integrate better with Async gem.
| ioquatix wrote:
| The gem namespace was transferred to me some time around
| 2017.
| Mikeb85 wrote:
| Rubygems.org has the first commit at 2013. Unless the gem
| changed ownership.
| brunosutic wrote:
| > the gem changed ownership
|
| I think the gem did change ownership at some point. The
| first couple gem releases from 2013 were yanked.
| Mikeb85 wrote:
| Different author credited but the same owner the whole
| time. So might be different. The library that async is
| based on is the same owner as well.
|
| Edit - the owner in 2013 is the same as the owner of the
| current gem and main contributor on Github. So yes it
| seems like it's been around since 2013.
| going_to_800 wrote:
| How this gem compares to other similar gems? just because it
| doesn't use threads?
| Mikeb85 wrote:
| There's lots of Ruby concurrency gems. OP makes it sound like
| it's a new runtime or paradigm... It's just a concurrency
| gem.
| michauu wrote:
| Could anyone comment on the thread safety? What's shared between
| async blocks and what's safe to modify?
| veesahni wrote:
| Multiple async blocks aren't really running concurrently
| because GIL is still in play. Control of what's running only
| switches if one async block yields... manually, or
| automatically. What's new is Ruby is auto-yielding when waiting
| on network or other blocking actions.
___________________________________________________________________
(page generated 2021-10-30 23:01 UTC)