> It absolutely is. Sure, you can safely call fread() from multiple threads, but they will wait for each other to finish, they will not be reading from the underlying file simultaneously.

What I took "synchronous" to mean was not whether FILE serializes access by multiple readers/writers (it does), but how the I/O operations are carried out. They are carried out a-synchronously in the sense that each fread() does not cause a corresponding read() syscall. Any fread() can cause 0, 1, or more such calls. In general, the hope is that it causes close to 0 system calls on average (that's the idea behind the buffering).

In some sense, the multiple threads will not be reading from the underlying file _at all_. To understand what I mean, imagine an implementation of FILE where the I/O is carried out by a worker thread, and connected to the FILE structure using a in-process queue and some thread synchronization (Mutex, Condition variable or similar). fread() only reads from the queue, and synchronizes to the condition variable only when not enough data is in the queue (just returning a short read instead of synchronizing and waiting for more data would be a more useful default option, but that is not how the simplistic FILE API is specified). In any case it never calls the OS for file I/O.

> But, in a non-concurrent context, you can always tell which fread() failed and why

I don't think you can do that unless the specs define precisely when the actually OS calls (read(2)) happen. Btw.
you might also see errors from filesystem operations that are not related to the particular file you were operating on - but due to some H/W problem, quota or filesystem full state, etc. What you're up to is not leading anywhere.

Anyway, my point is that this is almost completely meaningless, and definitely not how you are encouraged to use this API. Even the OS doesn't make particular guarantees what call an error that was returned to you is related to. For example, an error "resulting" from a write() might be visible only on close(). (Or in the worst case you never get to see it, but let's ignore that here).

What you _can_ do is call explicit synchronization routines (there is fflush() in case of FILE, but if you're getting serious you need to drop stdio and interface with the particular OS that you're running on). This lets you make inferences like, "everything I've written up to now is stored on disk".

Notice as well how your example of "tell which fread() failed" breaks down in the case of fwrite(), unless you do fflush() after every single fwrite(), which you absolutely are not advised to do because the temperature in my office has been already sufficient this summer.

There is an interesting asymmetry in fread() vs fwrite() with the usual implementations, which stems from the fact that blocking I/O is relatively "impure". A successful fread() implies that the data that crossed the FILE API has crossed the kernel/userspace boundary in the current call or in an earlier one. While an fwrite() implies the opposite - the data is transferred in the current or one of the next calls.

It would make a lot more sense if the OS would read/write the FILE buffers asynchronously, just like the worker thread example above. These days we're moving more and more to more explicitly decoupled asynchronous I/O, e.g. io_uring, and with these APIs there is a perfect symmetry between reading and writing, it's only a buffer exchange in both cases.

Even when you call fflush(), there is no guarantee that the data was written to disk. In general the data is only pushed to OS buffers, and will be committed to disk asynchronously. Consequently, there is no way to tell "which fwrite() failed".

In conclusion, this is a hopeless undertaking. What you're trying to do is resulting in unmaintainable and brittle code. Not too mention REALLY SLOW because whenever you're introducing synchronization everything is grinding to a halt. The tasks that we do on machines are nowhere near instant. Better imagine a computer as a house full of pipes connected to each other. All you can do in any particular location is pull a bit here, push a little there.