Thread-safe file objects, the return
Hello, It seems this subject has had quite a bit of history. Tim Peters demonstrated the problem in 2003 in this message: http://mail.python.org/pipermail/python-dev/2003-June/036537.html In short, Python file objects release the GIL before calling any C stdlib function on their embedded FILE pointer. Unfortunately, if another thread calls fclose on the FILE pointer concurrently, the contents pointed to can become garbage and the interpreter process crashes. Just by using the same file object in two threads running pure Python code, you can crash the interpreter. (another, easier-to-solve problem is that the FILE pointer stored in the file object could become NULL at the point it is used by another thread. If that was the only problem you could just store the FILE pointer in a local variable before releasing the GIL et voilà) There was some discussion at the time about the possible resolution. I've tried to fix the problem, and I've come to what I think is a satisfying solution, which I can sum up as the following bullet points: * Each file object gets a dedicated counter, which is incremented before the bject releases the GIL and decremented after the GIL is taken again; thus this counter keeps track of how many running "unlocked" sections of code are using that particular file object. (please note the counter doesn't need its own lock, since it is only modified in GIL-protected sections) * In the close() method, if the aforementioned counter is greater than 0, we refuse to call fclose and instead raise an IOError. This may seem like a worrying semantic change, but I don't think it is, for the following reasons: 1) if we closed the FILE pointer anyway, the interpreter would likely crash because another thread would be using garbage data (that's what we are trying to fix after all!) 2) if close() raises an IOError, it can be called again later, or at worse fclose will be called when the file object is garbage collected 3) close() can already raise an IOError if fclose fails for whatever reason (although for sure it's probably very rare) 4) it doesn't seem wrong to notify the programmer that his code is very unsafe The patch is attached at http://bugs.python.org/issue815646 . It addresses (or at least I hope it does) all potential problems with pure Python code, threads, and the file object. It doesn't try to fix C extensions using the PyFile_AsFile API and doing their own dirty things with the FILE pointer. It could be a second step if the approach is accepted, but as noted in the 2003 discussions it would probably involve a new API. Whether we want to introduce such an API in Python 2.x while Python 3.0 has a different IO model anyway is left open to discussion :) Regards Antoine.
This is not something that keeps me awake at night, but I am aware of
it. Your solution (a counter) seems fine except I think perhaps the
close() call should not raise IOError -- instead, it should set a flag
so that the thread that makes the counter go to zero can close the
thread (after all the file got closed while it was being used).
There are of course other concurrency issues besides close -- what if
two threads both try to do I/O on the file? What will the C stdio
library do in that case? Are stdio files thread-safe at the C level?
So (classically contradicting myself while I think the problem over
more) perhaps any I/O operation should be disallowed while the file is
in use by another thread?
--Guido
On Mon, Mar 31, 2008 at 1:09 PM, Antoine Pitrou
Hello,
It seems this subject has had quite a bit of history. Tim Peters demonstrated the problem in 2003 in this message: http://mail.python.org/pipermail/python-dev/2003-June/036537.html
In short, Python file objects release the GIL before calling any C stdlib function on their embedded FILE pointer. Unfortunately, if another thread calls fclose on the FILE pointer concurrently, the contents pointed to can become garbage and the interpreter process crashes. Just by using the same file object in two threads running pure Python code, you can crash the interpreter.
(another, easier-to-solve problem is that the FILE pointer stored in the file object could become NULL at the point it is used by another thread. If that was the only problem you could just store the FILE pointer in a local variable before releasing the GIL et voilà)
There was some discussion at the time about the possible resolution. I've tried to fix the problem, and I've come to what I think is a satisfying solution, which I can sum up as the following bullet points: * Each file object gets a dedicated counter, which is incremented before the bject releases the GIL and decremented after the GIL is taken again; thus this counter keeps track of how many running "unlocked" sections of code are using that particular file object. (please note the counter doesn't need its own lock, since it is only modified in GIL-protected sections) * In the close() method, if the aforementioned counter is greater than 0, we refuse to call fclose and instead raise an IOError.
This may seem like a worrying semantic change, but I don't think it is, for the following reasons: 1) if we closed the FILE pointer anyway, the interpreter would likely crash because another thread would be using garbage data (that's what we are trying to fix after all!) 2) if close() raises an IOError, it can be called again later, or at worse fclose will be called when the file object is garbage collected 3) close() can already raise an IOError if fclose fails for whatever reason (although for sure it's probably very rare) 4) it doesn't seem wrong to notify the programmer that his code is very unsafe
The patch is attached at http://bugs.python.org/issue815646 . It addresses (or at least I hope it does) all potential problems with pure Python code, threads, and the file object. It doesn't try to fix C extensions using the PyFile_AsFile API and doing their own dirty things with the FILE pointer. It could be a second step if the approach is accepted, but as noted in the 2003 discussions it would probably involve a new API. Whether we want to introduce such an API in Python 2.x while Python 3.0 has a different IO model anyway is left open to discussion :)
Regards
Antoine.
_______________________________________________ Python-Dev mailing list Python-Dev@python.org http://mail.python.org/mailman/listinfo/python-dev Unsubscribe: http://mail.python.org/mailman/options/python-dev/guido%40python.org
-- --Guido van Rossum (home page: http://www.python.org/~guido/)
On Tue, Apr 1, 2008 at 5:09 PM, Guido van Rossum
This is not something that keeps me awake at night, but I am aware of it. Your solution (a counter) seems fine except I think perhaps the close() call should not raise IOError -- instead, it should set a flag so that the thread that makes the counter go to zero can close the thread (after all the file got closed while it was being used).
No, raising IOError is the right thing to do here. The problem is that calling close on a file implies that the close actually completed in the OS when it returns. The call should not return until the file object has actually been closed in the underlying layers. You can't leave it for later to be done by one of the other currently operating threads as you violate the close semantics and lose direct indication of an error that occurred during close.
There are of course other concurrency issues besides close -- what if two threads both try to do I/O on the file? What will the C stdio library do in that case? Are stdio files thread-safe at the C level? So (classically contradicting myself while I think the problem over more) perhaps any I/O operation should be disallowed while the file is in use by another thread?
that does sound like the safest thing to do... -gps
Guido van Rossum
Your solution (a counter) seems fine except I think perhaps the close() call should not raise IOError -- instead, it should set a flag so that the thread that makes the counter go to zero can close the thread (after all the file got closed while it was being used).
I agree with Gregory: we should be explicit about what happens. I wonder what we would gain from that approach - apart from encouraging dangerous coding practices :) It also depends how far we want to go: I am merely proposing to fix the crashes, do we want to provide a "smarter" close() variation that does what you suggest for people that want (or need) to take the risk?
There are of course other concurrency issues besides close -- what if two threads both try to do I/O on the file? What will the C stdio library do in that case? Are stdio files thread-safe at the C level?
According to the glibc documentation, at http://www.gnu.org/software/libc/manual/html_node/Streams-and-Threads.html : « The POSIX standard requires that by default the stream operations are atomic. I.e., issuing two stream operations for the same stream in two threads at the same time will cause the operations to be executed as if they were issued sequentially. The buffer operations performed while reading or writing are protected from other uses of the same stream. To do this each stream has an internal lock object which has to be (implicitly) acquired before any work can be done. » So according to POSIX rules it should be perfectly safe. In any case, someone would have to try my patch under Windows and OS X and see if test_file.py passes without crashing. Regards Antoine.
On Wed, Apr 2, 2008 at 3:17 AM, Antoine Pitrou
Guido van Rossum
writes: Your solution (a counter) seems fine except I think perhaps the close() call should not raise IOError -- instead, it should set a flag so that the thread that makes the counter go to zero can close the thread (after all the file got closed while it was being used).
I agree with Gregory: we should be explicit about what happens. I wonder what we would gain from that approach - apart from encouraging dangerous coding practices :) It also depends how far we want to go: I am merely proposing to fix the crashes, do we want to provide a "smarter" close() variation that does what you suggest for people that want (or need) to take the risk?
I also agree with Gregory now -- at least on the issue of fclose(). I think that for other (non-closing) operations we should be fine, given the Posix requirement that streams have an internal lock. While multiple threads reading from a file sounds insane, multiple files writing to a file is pretty common (think of stdout or stderr) and should be supported.
There are of course other concurrency issues besides close -- what if two threads both try to do I/O on the file? What will the C stdio library do in that case? Are stdio files thread-safe at the C level?
According to the glibc documentation, at http://www.gnu.org/software/libc/manual/html_node/Streams-and-Threads.html :
« The POSIX standard requires that by default the stream operations are atomic. I.e., issuing two stream operations for the same stream in two threads at the same time will cause the operations to be executed as if they were issued sequentially. The buffer operations performed while reading or writing are protected from other uses of the same stream. To do this each stream has an internal lock object which has to be (implicitly) acquired before any work can be done. »
So according to POSIX rules it should be perfectly safe.
Cool.
In any case, someone would have to try my patch under Windows and OS X and see if test_file.py passes without crashing.
I know Windows has internal locks on stdio. I trust that OSX, being a BSD descendant, is posix compliant. So I'm not worried about these. +1 on your patch, assuming some other developer reviews it and submits it. -- --Guido van Rossum (home page: http://www.python.org/~guido/)
I've reviewed the patch on http://bugs.python.org/issue815646 and have
uploaded my modified version (mostly test improvements and some formatting
to keep C code under 80 columns with proper 8 space tabs). I would have
committed it already but I have a sneaking suspicion that its unit test will
barf on windows since it could depend on some posix-like file system
semantics.
Could someone with a windows build environment please test it as asked in
the issue and report back in the tracker?
thanks!
-gps
On Wed, Apr 2, 2008 at 11:47 AM, Guido van Rossum
On Wed, Apr 2, 2008 at 3:17 AM, Antoine Pitrou
wrote: Guido van Rossum
writes: Your solution (a counter) seems fine except I think perhaps the close() call should not raise IOError -- instead, it should set a flag so that the thread that makes the counter go to zero can close the thread (after all the file got closed while it was being used).
I agree with Gregory: we should be explicit about what happens. I wonder what we would gain from that approach - apart from encouraging dangerous coding practices :) It also depends how far we want to go: I am merely proposing to fix the crashes, do we want to provide a "smarter" close() variation that does what you suggest for people that want (or need) to take the risk?
I also agree with Gregory now -- at least on the issue of fclose().
I think that for other (non-closing) operations we should be fine, given the Posix requirement that streams have an internal lock. While multiple threads reading from a file sounds insane, multiple files writing to a file is pretty common (think of stdout or stderr) and should be supported.
There are of course other concurrency issues besides close -- what if two threads both try to do I/O on the file? What will the C stdio library do in that case? Are stdio files thread-safe at the C level?
According to the glibc documentation, at
http://www.gnu.org/software/libc/manual/html_node/Streams-and-Threads.html:
« The POSIX standard requires that by default the stream operations are atomic. I.e., issuing two stream operations for the same stream in two threads at the same time will cause the operations to be executed as if they were issued sequentially. The buffer operations performed while reading or writing are protected from other uses of the same stream. To
do
this each stream has an internal lock object which has to be (implicitly) acquired before any work can be done. »
So according to POSIX rules it should be perfectly safe.
Cool.
In any case, someone would have to try my patch under Windows and OS X and see if test_file.py passes without crashing.
I know Windows has internal locks on stdio. I trust that OSX, being a BSD descendant, is posix compliant. So I'm not worried about these.
+1 on your patch, assuming some other developer reviews it and submits it.
-- --Guido van Rossum (home page: http://www.python.org/~guido/http://www.python.org/%7Eguido/ ) _______________________________________________ Python-Dev mailing list Python-Dev@python.org http://mail.python.org/mailman/listinfo/python-dev Unsubscribe: http://mail.python.org/mailman/options/python-dev/greg%40krypto.org
participants (3)
-
Antoine Pitrou
-
Gregory P. Smith
-
Guido van Rossum