Skip to content

Commit e719617

Browse files
authored
Make LazySeq evaluation thread-safe (#935)
Fixes #934
1 parent 5c74ac1 commit e719617

File tree

2 files changed

+23
-18
lines changed

2 files changed

+23
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
* Fix a bug where reader column offset numbering began at 1, rather than 0 (#905)
2323
* Fix a bug where `basilisp.core/boolean` was returning the boolean coercions like Python rather than like Basilisp (#928)
2424
* Fix a bug where Basilisp vectors were not callable (#932)
25+
* Fix a bug where `basilisp.lang.seq.LazySeq` instances were not thread-safe (#934)
2526

2627
### Other
2728
* Add several sections to Concepts documentation module (#666)

src/basilisp/lang/seq.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import functools
2+
import threading
23
from typing import Callable, Iterable, Iterator, Optional, TypeVar, overload
34

45
from basilisp.lang.interfaces import (
@@ -152,7 +153,7 @@ class LazySeq(IWithMeta, ISequential, ISeq[T]):
152153
Callers should never provide the `obj` or `seq` arguments -- these are provided
153154
only to support `with_meta` returning a new LazySeq instance."""
154155

155-
__slots__ = ("_gen", "_obj", "_seq", "_meta")
156+
__slots__ = ("_gen", "_obj", "_seq", "_lock", "_meta")
156157

157158
def __init__(
158159
self,
@@ -165,6 +166,7 @@ def __init__(
165166
self._gen: Optional[LazySeqGenerator] = gen
166167
self._obj: Optional[ISeq[T]] = obj
167168
self._seq: Optional[ISeq[T]] = seq
169+
self._lock = threading.RLock()
168170
self._meta = meta
169171

170172
@property
@@ -206,22 +208,23 @@ def _compute_seq(self) -> Optional[ISeq[T]]:
206208
return self._obj if self._obj is not None else self._seq
207209

208210
def seq(self) -> Optional[ISeq[T]]:
209-
self._compute_seq()
210-
if self._obj is not None:
211-
o = self._obj
212-
self._obj = None
213-
# Consume any additional lazy sequences returned immediately so we have a
214-
# "real" concrete sequence to proxy to.
215-
#
216-
# The common idiom with LazySeqs is to return (cons value (lazy-seq ...))
217-
# from the generator function, so this will only result in evaluating away
218-
# instances where _another_ LazySeq is returned rather than a cons cell
219-
# with a concrete first value. This loop will not consume the LazySeq in
220-
# the rest position of the cons.
221-
while isinstance(o, LazySeq):
222-
o = o._compute_seq() # type: ignore
223-
self._seq = to_seq(o)
224-
return self._seq
211+
with self._lock:
212+
self._compute_seq()
213+
if self._obj is not None:
214+
o = self._obj
215+
self._obj = None
216+
# Consume any additional lazy sequences returned immediately, so we
217+
# have a "real" concrete sequence to proxy to.
218+
#
219+
# The common idiom with LazySeqs is to return
220+
# (cons value (lazy-seq ...)) from the generator function, so this will
221+
# only result in evaluating away instances where _another_ LazySeq is
222+
# returned rather than a cons cell with a concrete first value. This
223+
# loop will not consume the LazySeq in the rest position of the cons.
224+
while isinstance(o, LazySeq):
225+
o = o._compute_seq() # type: ignore
226+
self._seq = to_seq(o)
227+
return self._seq
225228

226229
@property
227230
def is_empty(self) -> bool:
@@ -246,7 +249,8 @@ def cons(self, elem):
246249

247250
@property
248251
def is_realized(self):
249-
return self._gen is None
252+
with self._lock:
253+
return self._gen is None
250254

251255

252256
def sequence(s: Iterable[T]) -> ISeq[T]:

0 commit comments

Comments
 (0)