diff -r afa295a98229 Lib/io.py --- a/Lib/io.py Thu Sep 04 13:24:53 2008 +0200 +++ b/Lib/io.py Thu Sep 04 16:05:13 2008 +0200 @@ -61,10 +61,49 @@ import _fileio import _fileio # Import _thread instead of threading to reduce startup cost try: - from _thread import allocate_lock as Lock + from _thread import allocate_lock as Lock, get_ident except ImportError: - from _dummy_thread import allocate_lock as Lock + from _dummy_thread import allocate_lock as Lock, get_ident + +class _RLock: + # A naive RLock implementation usable as a context manager. + # Since it is written in Python, it is still subject to some problems, + # e.g. when a tracing function tries to retake the lock while it is being + # taken. + + # Reasons why we need a recursive lock: BufferedReader and BufferedWriter + # are used for sys.{stdin,stdout,stderr}. As such, they may be called + # at any time (to print an unexpected exception, etc.) and thus must be + # reentrant. + + # Reasons why we don't use threading.RLock: + # 1. threading pulls many dependencies, which means a bigger executable + # and a longer startup time. + # 2. this implementation is twice faster. + + # NOTE: BufferedReader and BufferedWriter are reentrant only in the sense + # that they don't hang, crash or fail. There is no guarantee over the + # preservation of data when called in a reentrant way. + + def __init__(self): + self.lock = Lock() + self.owner = None + self.count = 0 + + def __enter__(self): + me = get_ident() + if self.owner != me: + self.lock.acquire() + self.owner = me + self.count += 1 + return self + + def __exit__(self, e, v, tb): + count = self.count = self.count - 1 + if count <= 0: + self.owner = None + self.lock.release() # open() uses st_blksize whenever we can DEFAULT_BUFFER_SIZE = 8 * 1024 # bytes @@ -899,7 +938,7 @@ class BufferedReader(_BufferedIOMixin): _BufferedIOMixin.__init__(self, raw) self.buffer_size = buffer_size self._reset_read_buf() - self._read_lock = Lock() + self._read_lock = _RLock() def _reset_read_buf(self): self._read_buf = b"" @@ -1025,7 +1064,7 @@ class BufferedWriter(_BufferedIOMixin): if max_buffer_size is None else max_buffer_size) self._write_buf = bytearray() - self._write_lock = Lock() + self._write_lock = _RLock() def write(self, b): if self.closed: diff -r afa295a98229 Lib/test/test_io.py --- a/Lib/test/test_io.py Thu Sep 04 13:24:53 2008 +0200 +++ b/Lib/test/test_io.py Thu Sep 04 16:05:13 2008 +0200 @@ -47,6 +47,28 @@ class MockRawIO(io.RawIOBase): def tell(self): return 42 + + +class MockRecursiveRawIO(MockRawIO): + rec = 0 + + def read(self, n=None): + if self.rec <= 3: + self.rec += 1 + try: + self.bufio.read(n) + finally: + self.rec -= 1 + return MockRawIO.read(self, n) + + def write(self, b): + if self.rec <= 3: + self.rec += 1 + try: + self.bufio.write(b) + finally: + self.rec -= 1 + return MockRawIO.write(self, b) class MockFileIO(io.BytesIO): @@ -434,6 +456,17 @@ class BufferedReaderTest(unittest.TestCa finally: support.unlink(support.TESTFN) + def testReentrancy(self): + # BufferedReader needs to be reentrant because it is used for stdin. + # We just test it doesn't crash or hang. + reader = MockRecursiveRawIO([b"abc", b"abcdefghi", b"abcde"]) + bufio = io.BufferedReader(reader, 8) + reader.bufio = bufio + + bufio.read(4) + bufio.read(9) + bufio.read(4) + bufio.close() class BufferedWriterTest(unittest.TestCase): @@ -522,6 +555,19 @@ class BufferedWriterTest(unittest.TestCa "the following exceptions were caught: %r" % errors) finally: support.unlink(support.TESTFN) + + def testReentrancy(self): + # BufferedWriter needs to be reentrant because it is used for stdout + # and stderr. We just test it doesn't crash or hang. + writer = MockRecursiveRawIO() + bufio = io.BufferedWriter(writer, 8) + writer.bufio = bufio + + bufio.write(b"abc") + bufio.flush() + bufio.write(b"abcdefghi") + bufio.flush() + bufio.close() class BufferedRWPairTest(unittest.TestCase):