Kaydet (Commit) aa63d0d4 authored tarafından Gregory P. Smith's avatar Gregory P. Smith

Make file objects as thread safe as the underlying libc FILE* implementation.

close() will now raise an IOError if any operations on the file object
are currently in progress in other threads.

Most code was written by Antoine Pitrou (pitrou).  Additional testing,
documentation and test suite cleanup done by me (gregory.p.smith).

Fixes issue 815646 and 595601 (as well as many other bugs and
references to this problem dating back to the dawn of Python).
üst ee767778
......@@ -58,10 +58,42 @@ change in future releases of Python.
closed. Return *NULL* on failure.
.. cfunction:: FILE* PyFile_AsFile(PyObject *p)
.. cfunction:: FILE* PyFile_AsFile(PyObject \*p)
Return the file object associated with *p* as a :ctype:`FILE\*`.
If the caller will ever use the returned :ctype:`FILE\*` object while
the GIL is released it must also call the `PyFile_IncUseCount` and
`PyFile_DecUseCount` functions described below as appropriate.
.. cfunction:: void PyFile_IncUseCount(PyFileObject \*p)
Increments the PyFileObject's internal use count to indicate
that the underlying :ctype:`FILE\*` is being used.
This prevents Python from calling f_close() on it from another thread.
Callers of this must call `PyFile_DecUseCount` when they are
finished with the :ctype:`FILE\*`. Otherwise the file object will
never be closed by Python.
The GIL must be held while calling this function.
The suggested use is to call this after `PyFile_AsFile` just before
you release the GIL.
.. versionadded:: 2.6
.. cfunction:: void PyFile_DecUseCount(PyFileObject \*p)
Decrements the PyFileObject's internal unlocked_count member to
indicate that the caller is done with its own use of the :ctype:`FILE\*`.
This may only be called to undo a prior call to `PyFile_IncUseCount`.
The GIL must be held while calling this function.
.. versionadded:: 2.6
.. cfunction:: PyObject* PyFile_GetLine(PyObject *p, int n)
......
......@@ -25,6 +25,8 @@ typedef struct {
int f_skipnextlf; /* Skip next \n */
PyObject *f_encoding;
PyObject *weakreflist; /* List of weak references */
int unlocked_count; /* Num. currently running sections of code
using f_fp with the GIL released. */
} PyFileObject;
PyAPI_DATA(PyTypeObject) PyFile_Type;
......@@ -38,6 +40,8 @@ PyAPI_FUNC(int) PyFile_SetEncoding(PyObject *, const char *);
PyAPI_FUNC(PyObject *) PyFile_FromFile(FILE *, char *, char *,
int (*)(FILE *));
PyAPI_FUNC(FILE *) PyFile_AsFile(PyObject *);
PyAPI_FUNC(void) PyFile_IncUseCount(PyFileObject *);
PyAPI_FUNC(void) PyFile_DecUseCount(PyFileObject *);
PyAPI_FUNC(PyObject *) PyFile_Name(PyObject *);
PyAPI_FUNC(PyObject *) PyFile_GetLine(PyObject *, int);
PyAPI_FUNC(int) PyFile_WriteObject(PyObject *, PyObject *, int);
......
import sys
import os
import unittest
import itertools
import time
import threading
from array import array
from weakref import proxy
from test import test_support
from test.test_support import TESTFN, findfile, run_unittest
from UserList import UserList
......@@ -339,11 +343,173 @@ class FileSubclassTests(unittest.TestCase):
self.failUnless(f.subclass_closed)
class FileThreadingTests(unittest.TestCase):
# These tests check the ability to call various methods of file objects
# (including close()) concurrently without crashing the Python interpreter.
# See #815646, #595601
def setUp(self):
self.f = None
self.filename = TESTFN
with open(self.filename, "w") as f:
f.write("\n".join("0123456789"))
self._count_lock = threading.Lock()
self.close_count = 0
self.close_success_count = 0
def tearDown(self):
if self.f:
try:
self.f.close()
except (EnvironmentError, ValueError):
pass
try:
os.remove(self.filename)
except EnvironmentError:
pass
def _create_file(self):
self.f = open(self.filename, "w+")
def _close_file(self):
with self._count_lock:
self.close_count += 1
self.f.close()
with self._count_lock:
self.close_success_count += 1
def _close_and_reopen_file(self):
self._close_file()
# if close raises an exception thats fine, self.f remains valid so
# we don't need to reopen.
self._create_file()
def _run_workers(self, func, nb_workers, duration=0.2):
with self._count_lock:
self.close_count = 0
self.close_success_count = 0
self.do_continue = True
threads = []
try:
for i in range(nb_workers):
t = threading.Thread(target=func)
t.start()
threads.append(t)
for _ in xrange(100):
time.sleep(duration/100)
with self._count_lock:
if self.close_count-self.close_success_count > nb_workers+1:
if test_support.verbose:
print 'Q',
break
time.sleep(duration)
finally:
self.do_continue = False
for t in threads:
t.join()
def _test_close_open_io(self, io_func, nb_workers=5):
def worker():
self._create_file()
funcs = itertools.cycle((
lambda: io_func(),
lambda: self._close_and_reopen_file(),
))
for f in funcs:
if not self.do_continue:
break
try:
f()
except (IOError, ValueError):
pass
self._run_workers(worker, nb_workers)
if test_support.verbose:
# Useful verbose statistics when tuning this test to take
# less time to run but still ensuring that its still useful.
#
# the percent of close calls that raised an error
percent = 100. - 100.*self.close_success_count/self.close_count
print self.close_count, ('%.4f ' % percent),
def test_close_open(self):
def io_func():
pass
self._test_close_open_io(io_func)
def test_close_open_flush(self):
def io_func():
self.f.flush()
self._test_close_open_io(io_func)
def test_close_open_iter(self):
def io_func():
list(iter(self.f))
self._test_close_open_io(io_func)
def test_close_open_isatty(self):
def io_func():
self.f.isatty()
self._test_close_open_io(io_func)
def test_close_open_print(self):
def io_func():
print >> self.f, ''
self._test_close_open_io(io_func)
def test_close_open_read(self):
def io_func():
self.f.read(0)
self._test_close_open_io(io_func)
def test_close_open_readinto(self):
def io_func():
a = array('c', 'xxxxx')
self.f.readinto(a)
self._test_close_open_io(io_func)
def test_close_open_readline(self):
def io_func():
self.f.readline()
self._test_close_open_io(io_func)
def test_close_open_readlines(self):
def io_func():
self.f.readlines()
self._test_close_open_io(io_func)
def test_close_open_seek(self):
def io_func():
self.f.seek(0, 0)
self._test_close_open_io(io_func)
def test_close_open_tell(self):
def io_func():
self.f.tell()
self._test_close_open_io(io_func)
def test_close_open_truncate(self):
def io_func():
self.f.truncate()
self._test_close_open_io(io_func)
def test_close_open_write(self):
def io_func():
self.f.write('')
self._test_close_open_io(io_func)
def test_close_open_writelines(self):
def io_func():
self.f.writelines('')
self._test_close_open_io(io_func)
def test_main():
# Historically, these tests have been sloppy about removing TESTFN.
# So get rid of it no matter what.
try:
run_unittest(AutoFileTests, OtherFileTests, FileSubclassTests)
run_unittest(AutoFileTests, OtherFileTests, FileSubclassTests,
FileThreadingTests)
finally:
if os.path.exists(TESTFN):
os.unlink(TESTFN)
......
......@@ -18,6 +18,11 @@ Extensions Modules
Library
-------
- Issue #815646: Individual file objects may now be used from multiple
threads at once without fear of crashing the Python interpreter. If
file.close() is called while an object is in use by another thread
an IOError exception will be raised and the file will not be closed.
- The bundled libffi copy is now in sync with the recently released
libffi3.0.5 version, apart from some small changes to
Modules/_ctypes/libffi/configure.ac.
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment