3 ms·
> fread() is an inherently synchronous operation NOOOO it's not. It's the opposite. FILE streams are buffered, and the typical case is that fread() just copies
by jstimpfle 4y ago
> fread() is an inherently synchronous operation
NOOOO it's not. It's the opposite. FILE streams are buffered, and the typical case is that fread() just copies from its internal buffer, where the data in the buffer had already been transferred from the OS at some earlier point. While it is not normally done, I think fread() could be implemented with a worker in a separate thread no problem. Think of it as an asynchronous API.
> if it would have made sense to call fread() on the same file handle concurrently, you would have to add synchronization logic (externally or internally) before checking the error flag, and even then, there would be no way to signal two different errors occurring in two different concurrent fread() calls - one error would always overwrite the other.
FILE streams are in fact protected by mutexes internally - it is safe to call e.g. fwrite() from different threads simultaneously (you can also circumvent the mutexes using e.g. fputc_unlocked() if you know it's safe to do so). Whether doing that makes sense beyond the quick debug printf is another question.
FILE streams are buffered, so you never know which fread() call was the "source" of the error. And there is absolutely no point at all in trying to relate errors on a file handle to the n'th or (n+1)'th call.
But if there was, the only thing you would have to do is to pass the function calls some context to discern between the calls. Essentially this is state segregation, which is not the point of the FILE API, which is why it doesn't do that.
> Sure, the concurrency implementation ultimately has to somehow store errors, but this doesn't have to be exposed to the client API.
Yes it does, unless you're willing to throw away the gains you got from concurrent operation (or with those fancy messy async frameworks, throw away at least a part of the gains). Retrieving the error or other return value requires synchronization.
> imagine if malloc() had a corresponding merror() like fread() to indicate why an allocation failed
You're not getting the point. malloc() API is fine for what it does (general purpose memory allocation in global context, which is not exactly what you should use in a well developed system, but like printf it gets you started...), and I'm not suggesting there should be a global merror() kind of thing.
But checkout the following:
mctx := malloc_context()
buffer1 := malloc_buffer(mctx, size1)
buffer2 := malloc_buffer(mctx, size2)
for i in 0...100:
buffers[i] := malloc_buffer(mctx, 42)
if mctx.bad():
mctx.release()
return
# go on using the buffers... Maybe even like follows?:
buffer_write(buffer1, data, 666)
buffer_write(buffer2, data, 1337)
if buffer1.bad():
# handle very specific problem?
if mctx.bad():
# handle problem with any buffer?
Better than checking each call individually, isn't it? Less code and faster to execute, because requests can be processed asynchronously. This is very much like the FILE API.
- simiones 4y ago> NOOOO it's not. 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. Also, it's not really possible to use this API thread-safely, especially in the presence of errors. For example, the following code may show exactly the problem I was talking about: void thread1() { int* buf1 = calloc(1000, sizeof(int)); while (true) { int read = fread(buf1, sizeof(int), 1000, file); if (read < 1000) { printf("couldn't read whole object: ") if (ferror(file)) { printf("read error"); } else if (feof(file)) { printf("end of file"); } else { printf("no reason???"); //will be occasionally printed } } else { printf("success"); } } } void thread2() { while (true) { fseek(file, 0, 0); } } > FILE streams are buffered, so you never know which fread() call was the "source" of the error. And there is absolutely no point at all in trying to relate errors on a file handle to the n'th or (n+1)'th call. The buffering is irrelevant. Sure, it's possible that ferror(f) is true even if your data was read successfully (a confusing situation in itself). But, in a non-concurrent context, you can always tell which fread() failed and why - the first time that you get back less bytes than you requested, if the file is not at the end, a read error was encountered (possibly some time ago, as you say). > Yes it does, unless you're willing to throw away the gains you got from concurrent operation (or with those fancy messy async frameworks, throw away at least a part of the gains). Retrieving the error or other return value requires synchronization. If you don't care about the result, than you don't even need to handle errors. If you do care about the result, returning the error in the exact same way as the success result adds 0 extra overhead/synchronization. However, returning the result in one place and the errors in another (like fread() does) adds mental overhead. > Better than checking each call individually, isn't it? Less code and faster to execute, because requests can be processed asynchronously. This is very much like the FILE API. It's easier to follow than manually checking each call individually, but I would say it's overall much worse: 1. If you forget to check for mctx.bad() before using the buffers, you'll have a very hard to track error. I would have to be very mindful of this during code review. 2. If you wanted to do something like initializing an array of buffers, you would have to check if mctx.bad() multiple times (once after malloc'ing the array, and again after malloc'ing all the buffers). 3. If I want to sequence these operations (write 1337 to buffer2 only if I successfully wrote 666 to buffer1) or need all of the data to be written or the whole operation is useless (and I don't want to wait to write 2GB to some file only to then find out that the 1B write to the registry failed), I actually have to write more code than if I had exceptions, since I have to add checks after each operation to interrupt the others. In contrast, here is how this would look like with C# async/await and typical exceptions: using(var mctx = malloc_context()) { var buffer1 = mctx.malloc_buffer(size1); //throws exception if it fails var buffer2 = mctx.malloc_buffer(size2); var buffers = mctx.malloc_buffer_array(100); //your solution was not allocating this, so didn't care about errors for (var i = 0; i < 100; i++) { buffers[i] = mctx.malloc_buffer(42); } //assuming we want the behavior you suggest, but buffer_write is sync Task.WaitAll(new Task[] { Task.Factory.StartNew(() => { try { buffer1.write(data, 666); catch(IOException e1) { //handle very specific problem? } }), Task.Factory.StartNew(() => buffer2.write(data, 1337)) }); //throws AggregateException if any of the tasks throws IOException //assuming we want to write all data and fail as soon as one thing fails: buffer1.write(data, 666); buffer2.write(data, 1337); } //error or no error, everything is cleaned up and an error is signalled to higher layers if this failed If you want asynchronous behavior, you should make it explicitly asynchronous.