4 ms·
I still think that most of this is irrelevant. Especially for fread(), all I care about when I'm doing error handling is "if I didn't get all the data I request
by simiones 4y ago
I still think that most of this is irrelevant. Especially for fread(), all I care about when I'm doing error handling is "if I didn't get all the data I requested for, why didn't I?" - if it's ferror() I may want to retry reading from the same place where my fread() began, but if it's feof() I probably don't. In this case, I can only meaningfully handle the error if I know exactly which of my fread()s failed for which reason.
Sure, the actual IO that caused that error may have happened 3 hours ago on a different thread, and 100 fread()s may have been able to read data from the buffer since then without caring about the IO error. When the 101st one actually sees it can't read enough, now I have to care, and I want to find out as easily as possible, and atomically. I don't want my (buffered) read to fail and then be unable to find out why because some other thread reset my context.
Now, for fwrite() the issue is indeed quite different, since my code doesn't depend on the result of the operation in the same way (I am ultimately invoking it for its side effect, not for its result, unlike fread() ). And you're right, by its very nature, fwrite() can't meaningfully report all errors I would care about (and even fflush() can't, as the creators of Postgres found out the hard way). But even here, the problem is not related in any way to how you chose to report exceptions, and ferror() is not in any way reliable for ensuring transactional access, any more than exceptions would be.
In fact, the newer async IO operations in both Windows and Linux (I/O Rings and io_uring) also return the result in place, not on some context object. Looking more closely at the Linux API, each CQE (completion queue event) has a `res` field that represents the error code from the results of 1 specific SQE (submission queue entry); this entirely replaces the state-based global errno variable that was sometimes used for other system calls. So, the pattern here is: start an async IO task, check the error returned in case it wasn't able to even start, wait for it to complete, check the error this specific task produced when it completed.
The same could be done with a hypothetical safe write operation: initiate a call to definitely_write(), and get back some kind of handle that allowed us to wait until those particular bytes were physically written to disk (or at the very least to the disk driver, since those now often do their own level of buffering...). The wait() operation on this handle would then return an error code if the write failed for any reason. Of course, in the case of an actual IO error, it's very likely that many different such handles will return the same error when someone waits for them to finish, since the error itself was not specific to some specific byte. And also of course, it is possible that the bytes from one particular definitely_write() that returns an error on wait() have actually been written to disk - the error just means that the IO layer can't guarantee this to us.
But even so, there's no reason and no benefit to decoupling the error reporting from the operations themselves, especially if the error state is transient - and all modern IO APIs, low or high level, recognize this.
- jstimpfle 4y agoLooks like we're on the same page now :-). > and then be unable to find out why because some other thread reset my context. Then don't do that. Don't reset the context in another thread without coordination. What is the definition of a "modern" IO API? It would be unreasonable to argue that streaming APIs like FILE are ancient, or not modern. FILE is a streaming abstraction that allows to read streams (such as files on a filesystem on a hard disk are, for example) in arbitrary chunks, even byte-by-byte. The buffering in a shared (ring-) buffer and the access to it in arbitrary chunks represents an abstraction over the system call level. The (submission) buffers are abstracted away. It would be extremely uneconomical to persist explicit request states, and expect status reports, for 1-byte reads. Instead, the abstraction simply accesses a ringbuffer. Let me make some concessions, since I loved my role as an adversary in this discussion a little too much last night. There certainly are situations where system calls are just the right level of abstraction. They are obviously what you use if you implement an abstraction on top. Maybe also suitable for some kinds of forensic software. Or if you have specific performance requirements. If you need close control over system calls and need status reports for individual requests, this is the level at which to write your application. Also, it's obviously true that data that was returned to you from fread() has been transferred from the disk to userspace successfully. I don't know if I tried to dispute that, but if I did I surely shouldn't have attempted. What I was at though is that you aren't well advised to rely on any correlation between fread() calls and system calls (unless you disable the buffering in the FILE handle, btw.). This is simply a different level of abstraction. It might be unwise as well to to try and associate a positive ferror() state to any particular fread() or fwrite(). I reiterate that streaming APIs, whether blocking (like FILE) or non-blocking, are a totally valid and practical abstraction. They allow reusing a generic buffering and OS layer, and enable the design of concise application code. They should be the first abstraction to consider for most types of software. For example, as long as you only care whether a file was completely committed to disk or not, which is an extremely common case, just fopen()/fwrite().../fflush()/ferror()/fclose() sequence is enough and the FILE API serves you perfectly. > The same could be done with a hypothetical safe write operation: initiate a call to definitely_write(), and get back some kind of handle that allowed us to wait until those particular bytes were physically written to disk (or at the very least to the disk driver, since those now often do their own level of buffering...). Modern approaches take a different route about synchronization and transaction handling: What you actually need is ordering constraints. Write this only after that other thing was completely committed. Read A only after B was successfully read. Ordering is completely sufficient for these tasks. There is rarely a need to synchronize, which is coarse-grained and very slow. So, things like fflush() and fsync() (the latter is a system call) can maybe considered ancient. I don't know what modern APIs are available to achieve ordering of I/O operations, but this is the way it works for thread synchronization over CPU / memory these days. In conclusion, again, whenever you wait for an error (or any status result really) from an operation that is executed asynchronously, this is _slooow_. (Caveats apply wrt. FILE because it is in some sense a blocking abstraction over an in-spirit asynchronous operation (OS I/O)). Avoid synchronizing on status values (In the case of FILE, avoid fflush()). In most cases, you can easily read them back asynchronously, at most synchronizing at the very end of a larger "batch" of work.