8 ms·
The Various Kinds of IO – Blocking, Non-Blocking, Multiplexed and Async
- ioquatix 9y agoThere are three main kinds of async IO. * Event driven with callbacks (`on data`, `on error`, and so on). * Stackless coroutines (async/await). * Stackful coroutines (no direct semantic changes to code required). Personally, I like stackful coroutines because the concurrency model has minimal effect on the structure of the code, i.e. it's possible to retrofit existing code without change.
- girvo 9y agoWhat’s a good example implementation of stackful coroutines?
- RussianCow 9y agoAny sort of "green thread" implementation, such as Python's greenlets.
- ufo 9y agoLua coroutines work like this.
- anonacct37 9y agoWould golang count?
- ioquatix 9y agoYes go would fit into the green thread bucket pretty well.
- ioquatix 9y agoThere are two kinds of stackful coroutines - fibers and green threads. The difference is that fibers stay on the thread they were created on, while green threads can transit between OS threads. There have also been projects to cooperatively schedule OS level threads to minimise latency, but that's a bit different. I don't know that many decent green thread implementations [off the top of my head], but an interesting one is yahns: https://yhbt.net/yahns/design_notes.txt https://yhbt.net/yahns/design_notes.txt - to me, green threads have all the benefits of coroutines but all of the downsides of multi-threaded programming. I implemented fiber based concurrency for C++ here: https://github.com/kurocha/async https://github.com/kurocha/async. An example would be https://github.com/kurocha/async-network/blob/d1d2bf12eb2d9b259ecc59f2e4a5cf0756c441e4/test/Async/Network/Test.Socket.cpp#L55 https://github.com/kurocha/async-network/blob/d1d2bf12eb2d9b... - calling read yields the fiber if it would block as implemented here: https://github.com/kurocha/async/blob/2edef4d6990259cc60cc307b6de2ab35b97560f1/source/Async/Protocol/Stream.cpp#L33-L54 https://github.com/kurocha/async/blob/2edef4d6990259cc60cc30... I implemented almost exactly the same model for Ruby here: https://github.com/socketry/async https://github.com/socketry/async and if you are interested in how it works, see these projects which build on it: https://github.com/socketry/async#see-also https://github.com/socketry/async#see-also
- Tarean 9y agoHaskell does async IO by default using green threads. GHC allocates Thread State Objects which completely represent a (green) thread on the heap. A capabillity which represents a cpu core contains a queue of TSO's. If a thread blocks on something it can also move to a different queue which is used for thread synchronization, blocking on computations from other threads, io etc.
- samstave 9y agoOk. Im going to ask you a favor, because i love this comment so much based on how info-dense it is. I challenge you to unpack it not as an ELI5 - but ELIInfant. Please accept. Many shall learn from such.
- ioquatix 9y agoI particularly like this video. Start watching from here: https://www.youtube.com/watch?v=_fu0gx-xseY&feature=youtu.be&t=1m54s https://www.youtube.com/watch?v=_fu0gx-xseY&feature=youtu.be... for the historical context/concepts. He gives an absolutely brilliant overview, better than I can do here with a wall of text. However, he is an advocate for stackless coroutines. They are a good idea in some situations but they still have a semantic overhead unlike stackful coroutines. They may have slightly better performance but at the expense of semantic overhead (async/await, explicit futures/concurrency). I like to think that I'm not advocating for a specific design choice, but for a design choice which makes it possible to write higher level abstractions without the need to expose the underlying concurrency models. If the video doesn't answer all your questions, I'd be happy to answer specific questions.
- rubiquity 9y agoAs I understand it, stackless and stackful coroutines are an abstraction used on top of evented IO. They all use epoll, kqueue, etc. under the covers.
- SideburnsOfDoom 9y agoMy understanding is the same - in C#, async and await are syntactic sugar for callbacks. The code is massively transformed into a state machine for the events.
- ioquatix 9y agoCoroutines are useful in their own right, too.
- dnomad 9y agoI think stackful coroutines will always have their fans but the industry is heading towards stackless. The reality is that blocking I/O is a dangerous illusion. Any time you leave your address space you are fundamentally engaged in an asynchronous exchange. APIs that hide this from people -- even in the name of "ease of use" -- are simply inviting all sorts of unexpected behavior or worse, deadlocks. Stackful coroutines hide their blocking nature from the caller and this makes it very difficult to reason about their behavior.
- zaarn 9y agoYou can get deadlocks in stackless coroutines too. The reason node.js won't deadlock is that it is single threaded. I find stackful coroutines (ie, Go, Rust, Python Async, etc.) quite useful, they don't behave differently to normal functions and in most languages also don't require callbacks (you can exchange channels, generators, etc.). Additionally, such actually threaded implementations of coroutines can take advantage of multiple CPU cores more easily, goroutines can swap quickly in and out of CPU threads. Any waiting action will suspend the goroutine until the IO is available. Unlike in Node.js it won't have to wait until the current synchronous action is done since it can be scheduled in another goroutine at any time (and goroutines automatically yield regularly or you put in manual yields). There is nothing wrong with hiding the nature of the async under the hood of a sync function as long as the abstraction is clean, which in most cases it quite easily can be. For some cases you'll obviously need locks of course, but a simply RWMutex/RWLock can prevent deadlocks easily for 99% of situations where you will need it.
- xfer 9y agoRust allows you to implement bot stackless/stackful coroutines. But the popular library(futures) is stackless and there are proposals to make stackless coroutines convenient(generators RFC).
- beagle3 9y ago> I find stackful coroutines (ie, Go, Rust, Python Async, etc.) quite useful,they don't behave differently to normal functions Python Async is "stackless coroutine", not "stackful" according to the taxonomy given by GP. They also behave quite differently from normal functions.
- beagle3 9y agoThe stackful coroutine route is ideal on paper, but in practice it tends to perform poorly if you retrofit existing code -- because quite often, that existing code depends on blocking i/o or relatively long computations; e.g. opening a file, reading data from a database, or waiting for a TCP connection on the other side of the world to yield data. It is, of course, possible to wrap all of them in a stackful coroutine interface - but that is no longer "retrofitting existing code without change". My impression is that the cumulative experience is to go either with event driven (in C / C++ / Python) or Stackless (in Python / JavaScript) or threaded (in Java), but stackful coroutines very rarely deliver on their promise.
- adamnemecek 9y agoAlso vectored and ninety mapped but that’s in a somewhat different category.
- RonInDune 9y agoWhere is vectored I/O used in practice? I'd think the requirement of using multiple buffers for scatter/gather is not very efficient...
- zAy0LfpBZLC8mAC 9y agoThe point isn't the requirement, the point is the possibility. You can supply a vector with just one element if you like. But if the data is already in multiple buffers, you avoid either a copy or the overhead of additional syscalls. Typical use cases are when you have headers and payload of some protocol in different buffers, possibly of multiple layers of protocol headers.
- ramchip 9y agoErlang iolists are an example. It means you can generate HTML from templates as a deeply nested list of strings, and write that to a socket efficiently, without concatenating the strings.
- kccqzy 9y agoOne thing I'd like to add to the discussion is the difference between edge-triggered and level-triggered notifications. Quoting from the Linux Programming Interface (Kerrisk, 2010), > Level-triggered notification: A file descriptor is considered to be ready if it is possible to perform an I/O system call without blocking. > Edge-triggered notification: Notification is provided if there is I/O activity (e.g., new input) on a file descriptor since it was last monitored. The two different types can affect the way the program is structured. For example, the same book says that for level-triggered notification, generally when a file descriptor is ready, only one read/write operation is performed, and then go back to the main loop. Whereas for edge-triggered, usually I/O is performed as much as possible, so we don't miss any more opportunities. In practice, you usually want your file descriptors to be non-blocking, for many reasons (like writing a large enough buffer can nevertheless block even when the file descriptor is initially considered ready for writing), so even for level-triggered notifications you can read/write in a loop. Personally I believe edge-triggered notifications can make program design slightly simpler, though I'm not exactly uncertain how much simpler. I'd appreciate if my comment would invite a more detailed and nuanced discussion about those two.
- ajross 9y ago> In practice, you usually want your file descriptors to be non-blocking, for many reasons I think I know what you're saying, but in practice this is exactly backwards. Usually you're doing I/O for some practical reason and want to do simple, well-defined, typically sequential processing on the results. Which is to say blocking is your friend and you shouldn't be mucking with parallel I/O paradigms if you can at all avoid it.
- ioquatix 9y agoStackful fibers allow you to have concurrency with the illusion of blocking IO.
- kccqzy 9y agoYes. The default of blocking I/O is optimized for simple programs where I/O is not the main thing. That default is perfect for those programs. Another default behavior, killing the program on SIGPIPE is also optimized for those programs. But I'm specifically talking about those that need sophisticated strategies to deal with multiplexed I/O (which is a topic of this article you're commenting on).
- erikb 9y agoIt's a little bad for newcomers to define these as one being better than the other. In reality it's a trade-off. And multiplexed/async are not really "I/O types" but ways to handle the two I/O types. In reality everything is either blocking or non-blocking and you always need to consider which option is better for your current usecase. Even the most fancy of web frameworks won't take that out of the equation.
- magnat 9y ago> On the IO completing, the OS will suspend your thread, and execute your callback. On Windows, you have to put your thread to sleep to receive any callbacks [1]. If OS would suspend your thread at random point to execute a callback, that could lead to hard-to-detect/debug deadlocks and race conditions. [1] https://msdn.microsoft.com/en-us/library/windows/desktop/aa364052(v=vs.85).aspx https://msdn.microsoft.com/en-us/library/windows/desktop/aa3...
- known 9y agohttp://www.kegel.com/c10k.html http://www.kegel.com/c10k.html FTW
- lettergram 9y agoI actually have a little repo that has many of the ways you can have IPC (interprocess communication): https://github.com/lettergram/IPC-examples https://github.com/lettergram/IPC-examples Lots of fun to play with
- kbumsik 9y agoI think there are some misleading points in a hardware perspective at intro. > That is, gone are the days of serial communication. In that sense, all communications between the CPU and peripherals is therefore asynchronous at a hardware level. At a hardware level, 99.99% of modern peripherals are based on serial communication (e.g. USB, PCIe, I2C...), and these serial comminications are of course fully asynchronous. They implement concurrent communication at a higher level. > Think of the simple case, where the CPU would ask the peripheral to read something, and it would then go in an infinite loop, Many peripherals are logic blocks that doesn’t infinite loop internally. Infinite loop is a kind of software techniques and they don’t need to implement looping to do their job.
- corysama 9y agoIn both of your arguments you are disagreeing because you are not talking the same thing that the author is talking about. You are talking about what the hardware does behind the scenes. He is talking about what is going on in the software on the surface. “serial communication” should maybe be rephrased as “serialized communication and execution” in the application process.
- jerf 9y agoI feel like one critical misunderstanding people have is left out in between the hardware and software discussion, which is that the kernel provides certain abstractions, and everything else is built on top of that. Whether or not a program is blocking or non-blocking or multiplexed or whathaveyou is an effect depends on the layer of abstraction you are looking at. It is perfectly possible to have a programming language that is blocking that, if you strace it, is using the non-blocking kernel calls. It is perfectly possible to have an "async" layer under the hood and implement a "non-blocking" layer on top of it, and then on top of that you could easily (even accidentally!) implement a blocking layer of abstraction. It's a lot more a continuum than people realize. I also got the sense during the height of the Node craze that some of the people who were very excited about it had the impression that Node had some sort of unique access to a "non-blocking" kernel layer or something (of course, they just weren't thinking about the kernel layer at all) that no other languages were using. In reality, if you set something like a Go program to a single OS thread for execution, you could mechanically translate a Go program into something that executes in principle identically to a Node program at the assembler level, at least in terms of what events come in and what sort of high-level code gets executed in response (vast differences in the literal instruction stream, of course). The converted Go would have a lot more "event handlers" than you'd expect because Go programs also break at function calls and a few other places (as the compiler is doing it it's easy to have lots of "event break points" that you'd never write by hand), but the compiler essentially converts the "blocking code" that uses lots of threads into "non-blocking event-based code". The difference between the two is not at the execution layer; it's at the layer you're programming. In fact pretty much everything nowadays is "non-blocking event-based code" at the execution layer, because the high-powered kernel functions that make that efficient implement event-based code, so anything else you find at a programming-language-level must be converting that abstraction into the language's abstraction. Even using kernel threads still turns into non-blocking event-based code under the hood, if you dig far enough in. (A more clear example of how critical the layer of abstraction you are looking at is IMHO is the distinction between immutable and mutable. You can implement an immutable abstraction layer on top of mutable storage, which given that our hardware is based on mutable storage, all immutability in programming languages necessarily comes from. You can implement a mutable abstraction on top of an immutable layer with a worst-case penalty of O(log n), as in the worst case you represent memory as a immutable tree of bytes and then mutate as you would normally. Whether or not something is "mutable" or "immutable" critically depends on layer of abstraction you are looking at; it does not have a non-contingent answer, or, if it does, since our hardware is mutable the answer is that immutability does not exist at all. But while true in a sense, that answer is not as useful as one that is contingent on the abstraction layer being examined.)