Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8769576

Browse files
committed
Merged revisions 65686 via svnmerge from
svn+ssh://[email protected]/python/trunk ........ r65686 | antoine.pitrou | 2008-08-14 23:04:30 +0200 (jeu., 14 août 2008) | 3 lines Issue #3476: make BufferedReader and BufferedWriter thread-safe ........
1 parent 74bbea7 commit 8769576

4 files changed

Lines changed: 159 additions & 41 deletions

File tree

Lib/io.py

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import codecs
6262
import _fileio
6363
import warnings
64+
import threading
6465

6566
# open() uses st_blksize whenever we can
6667
DEFAULT_BUFFER_SIZE = 8 * 1024 # bytes
@@ -895,6 +896,7 @@ def __init__(self, raw, buffer_size=DEFAULT_BUFFER_SIZE):
895896
_BufferedIOMixin.__init__(self, raw)
896897
self.buffer_size = buffer_size
897898
self._reset_read_buf()
899+
self._read_lock = threading.Lock()
898900

899901
def _reset_read_buf(self):
900902
self._read_buf = b""
@@ -908,6 +910,10 @@ def read(self, n=None):
908910
mode. If n is negative, read until EOF or until read() would
909911
block.
910912
"""
913+
with self._read_lock:
914+
return self._read_unlocked(n)
915+
916+
def _read_unlocked(self, n=None):
911917
nodata_val = b""
912918
empty_values = (b"", None)
913919
buf = self._read_buf
@@ -960,6 +966,10 @@ def peek(self, n=0):
960966
do at most one raw read to satisfy it. We never return more
961967
than self.buffer_size.
962968
"""
969+
with self._read_lock:
970+
return self._peek_unlocked(n)
971+
972+
def _peek_unlocked(self, n=0):
963973
want = min(n, self.buffer_size)
964974
have = len(self._read_buf) - self._read_pos
965975
if have < want:
@@ -976,18 +986,21 @@ def read1(self, n):
976986
# only return buffered bytes. Otherwise, we do one raw read.
977987
if n <= 0:
978988
return b""
979-
self.peek(1)
980-
return self.read(min(n, len(self._read_buf) - self._read_pos))
989+
with self._read_lock:
990+
self._peek_unlocked(1)
991+
return self._read_unlocked(
992+
min(n, len(self._read_buf) - self._read_pos))
981993

982994
def tell(self):
983995
return self.raw.tell() - len(self._read_buf) + self._read_pos
984996

985997
def seek(self, pos, whence=0):
986-
if whence == 1:
987-
pos -= len(self._read_buf) - self._read_pos
988-
pos = self.raw.seek(pos, whence)
989-
self._reset_read_buf()
990-
return pos
998+
with self._read_lock:
999+
if whence == 1:
1000+
pos -= len(self._read_buf) - self._read_pos
1001+
pos = self.raw.seek(pos, whence)
1002+
self._reset_read_buf()
1003+
return pos
9911004

9921005

9931006
class BufferedWriter(_BufferedIOMixin):
@@ -1009,43 +1022,51 @@ def __init__(self, raw,
10091022
if max_buffer_size is None
10101023
else max_buffer_size)
10111024
self._write_buf = bytearray()
1025+
self._write_lock = threading.Lock()
10121026

10131027
def write(self, b):
10141028
if self.closed:
10151029
raise ValueError("write to closed file")
10161030
if isinstance(b, str):
10171031
raise TypeError("can't write str to binary stream")
1018-
# XXX we can implement some more tricks to try and avoid partial writes
1019-
if len(self._write_buf) > self.buffer_size:
1020-
# We're full, so let's pre-flush the buffer
1021-
try:
1022-
self.flush()
1023-
except BlockingIOError as e:
1024-
# We can't accept anything else.
1025-
# XXX Why not just let the exception pass through?
1026-
raise BlockingIOError(e.errno, e.strerror, 0)
1027-
before = len(self._write_buf)
1028-
self._write_buf.extend(b)
1029-
written = len(self._write_buf) - before
1030-
if len(self._write_buf) > self.buffer_size:
1031-
try:
1032-
self.flush()
1033-
except BlockingIOError as e:
1034-
if (len(self._write_buf) > self.max_buffer_size):
1035-
# We've hit max_buffer_size. We have to accept a partial
1036-
# write and cut back our buffer.
1037-
overage = len(self._write_buf) - self.max_buffer_size
1038-
self._write_buf = self._write_buf[:self.max_buffer_size]
1039-
raise BlockingIOError(e.errno, e.strerror, overage)
1040-
return written
1032+
with self._write_lock:
1033+
# XXX we can implement some more tricks to try and avoid
1034+
# partial writes
1035+
if len(self._write_buf) > self.buffer_size:
1036+
# We're full, so let's pre-flush the buffer
1037+
try:
1038+
self._flush_unlocked()
1039+
except BlockingIOError as e:
1040+
# We can't accept anything else.
1041+
# XXX Why not just let the exception pass through?
1042+
raise BlockingIOError(e.errno, e.strerror, 0)
1043+
before = len(self._write_buf)
1044+
self._write_buf.extend(b)
1045+
written = len(self._write_buf) - before
1046+
if len(self._write_buf) > self.buffer_size:
1047+
try:
1048+
self._flush_unlocked()
1049+
except BlockingIOError as e:
1050+
if len(self._write_buf) > self.max_buffer_size:
1051+
# We've hit max_buffer_size. We have to accept a
1052+
# partial write and cut back our buffer.
1053+
overage = len(self._write_buf) - self.max_buffer_size
1054+
self._write_buf = self._write_buf[:self.max_buffer_size]
1055+
raise BlockingIOError(e.errno, e.strerror, overage)
1056+
return written
10411057

10421058
def truncate(self, pos=None):
1043-
self.flush()
1044-
if pos is None:
1045-
pos = self.raw.tell()
1046-
return self.raw.truncate(pos)
1059+
with self._write_lock:
1060+
self._flush_unlocked()
1061+
if pos is None:
1062+
pos = self.raw.tell()
1063+
return self.raw.truncate(pos)
10471064

10481065
def flush(self):
1066+
with self._write_lock:
1067+
self._flush_unlocked()
1068+
1069+
def _flush_unlocked(self):
10491070
if self.closed:
10501071
raise ValueError("flush of closed file")
10511072
written = 0
@@ -1064,8 +1085,9 @@ def tell(self):
10641085
return self.raw.tell() + len(self._write_buf)
10651086

10661087
def seek(self, pos, whence=0):
1067-
self.flush()
1068-
return self.raw.seek(pos, whence)
1088+
with self._write_lock:
1089+
self._flush_unlocked()
1090+
return self.raw.seek(pos, whence)
10691091

10701092

10711093
class BufferedRWPair(BufferedIOBase):
@@ -1155,7 +1177,8 @@ def seek(self, pos, whence=0):
11551177
# First do the raw seek, then empty the read buffer, so that
11561178
# if the raw seek fails, we don't lose buffered data forever.
11571179
pos = self.raw.seek(pos, whence)
1158-
self._reset_read_buf()
1180+
with self._read_lock:
1181+
self._reset_read_buf()
11591182
return pos
11601183

11611184
def tell(self):
@@ -1192,8 +1215,9 @@ def read1(self, n):
11921215
def write(self, b):
11931216
if self._read_buf:
11941217
# Undo readahead
1195-
self.raw.seek(self._read_pos - len(self._read_buf), 1)
1196-
self._reset_read_buf()
1218+
with self._read_lock:
1219+
self.raw.seek(self._read_pos - len(self._read_buf), 1)
1220+
self._reset_read_buf()
11971221
return BufferedWriter.write(self, b)
11981222

11991223

Lib/test/test_cmd_line.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
# See test_cmd_line_script.py for testing of script execution
44

55
import test.support, unittest
6+
import os
67
import sys
78
import subprocess
89

910
def _spawn_python(*args):
10-
cmd_line = [sys.executable, '-E']
11+
cmd_line = [sys.executable]
12+
# When testing -S, we need PYTHONPATH to work (see test_site_flag())
13+
if '-S' not in args:
14+
cmd_line.append('-E')
1115
cmd_line.extend(args)
1216
return subprocess.Popen(cmd_line, stdin=subprocess.PIPE,
1317
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
@@ -59,6 +63,16 @@ def test_q(self):
5963
self.verify_valid_flag('-Qwarnall')
6064

6165
def test_site_flag(self):
66+
if os.name == 'posix':
67+
# Workaround bug #586680 by adding the extension dir to PYTHONPATH
68+
from distutils.util import get_platform
69+
s = "./build/lib.%s-%.3s" % (get_platform(), sys.version)
70+
if hasattr(sys, 'gettotalrefcount'):
71+
s += '-pydebug'
72+
p = os.environ.get('PYTHONPATH', '')
73+
if p:
74+
p += ':'
75+
os.environ['PYTHONPATH'] = p + s
6276
self.verify_valid_flag('-S')
6377

6478
def test_usage(self):

Lib/test/test_io.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import sys
55
import time
66
import array
7+
import threading
8+
import random
79
import unittest
8-
from itertools import chain
10+
from itertools import chain, cycle
911
from test import support
1012

1113
import codecs
@@ -390,6 +392,49 @@ def testFilenoNoFileno(self):
390392
# this test. Else, write it.
391393
pass
392394

395+
def testThreads(self):
396+
try:
397+
# Write out many bytes with exactly the same number of 0's,
398+
# 1's... 255's. This will help us check that concurrent reading
399+
# doesn't duplicate or forget contents.
400+
N = 1000
401+
l = list(range(256)) * N
402+
random.shuffle(l)
403+
s = bytes(bytearray(l))
404+
with io.open(support.TESTFN, "wb") as f:
405+
f.write(s)
406+
with io.open(support.TESTFN, "rb", buffering=0) as raw:
407+
bufio = io.BufferedReader(raw, 8)
408+
errors = []
409+
results = []
410+
def f():
411+
try:
412+
# Intra-buffer read then buffer-flushing read
413+
for n in cycle([1, 19]):
414+
s = bufio.read(n)
415+
if not s:
416+
break
417+
# list.append() is atomic
418+
results.append(s)
419+
except Exception as e:
420+
errors.append(e)
421+
raise
422+
threads = [threading.Thread(target=f) for x in range(20)]
423+
for t in threads:
424+
t.start()
425+
time.sleep(0.02) # yield
426+
for t in threads:
427+
t.join()
428+
self.assertFalse(errors,
429+
"the following exceptions were caught: %r" % errors)
430+
s = b''.join(results)
431+
for i in range(256):
432+
c = bytes(bytearray([i]))
433+
self.assertEqual(s.count(c), N)
434+
finally:
435+
support.unlink(support.TESTFN)
436+
437+
393438

394439
class BufferedWriterTest(unittest.TestCase):
395440

@@ -446,6 +491,38 @@ def testFlush(self):
446491

447492
self.assertEquals(b"abc", writer._write_stack[0])
448493

494+
def testThreads(self):
495+
# BufferedWriter should not raise exceptions or crash
496+
# when called from multiple threads.
497+
try:
498+
# We use a real file object because it allows us to
499+
# exercise situations where the GIL is released before
500+
# writing the buffer to the raw streams. This is in addition
501+
# to concurrency issues due to switching threads in the middle
502+
# of Python code.
503+
with io.open(support.TESTFN, "wb", buffering=0) as raw:
504+
bufio = io.BufferedWriter(raw, 8)
505+
errors = []
506+
def f():
507+
try:
508+
# Write enough bytes to flush the buffer
509+
s = b"a" * 19
510+
for i in range(50):
511+
bufio.write(s)
512+
except Exception as e:
513+
errors.append(e)
514+
raise
515+
threads = [threading.Thread(target=f) for x in range(20)]
516+
for t in threads:
517+
t.start()
518+
time.sleep(0.02) # yield
519+
for t in threads:
520+
t.join()
521+
self.assertFalse(errors,
522+
"the following exceptions were caught: %r" % errors)
523+
finally:
524+
support.unlink(support.TESTFN)
525+
449526

450527
class BufferedRWPairTest(unittest.TestCase):
451528

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ Core and Builtins
3030
Library
3131
-------
3232

33+
- Issue #3476: binary buffered reading through the new "io" library is now
34+
thread-safe.
35+
3336
- Issue #1342811: Fix leak in Tkinter.Menu.delete. Commands associated to
3437
menu entries were not deleted.
3538

0 commit comments

Comments
 (0)