7 ms·
- 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
by 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.
- pqdbr 5y agoI have the same question as parent and I'd love to read more about this distinction. Please do blog about it.
- Matthias247 5y agoIs the scheduler only using a single OS thread? It's not M:N scheduling?
- gpderetta 5y agoCooperative 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 5y ago> 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 5y agoYep… 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.
- YorickPeterse 5y ago> 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 5y ago> 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 5y ago> 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.
- grncdr 5y agoWhile true, the same would apply to code using Async blocks. Async does not attempt (or intend) to enable parallel execution of VM byte code. If the parent commenter saw a speed up in their tests, it’s reasonable to assume that is because of the difference in overhead of switching threads vs fibers.
- Yoric 5y ago> - 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.
- brunosutic 5y ago> 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.
- trinovantes 5y agoYou 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)
- 5y ago