Skip to content

Commit d347428

Browse files
committed
Document atomic promises
1 parent e9eac0d commit d347428

File tree

3 files changed

+128
-10
lines changed

3 files changed

+128
-10
lines changed

doc/reference/gerbil/core/expression.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,27 @@ macro.
169169
(delay expr)
170170
```
171171

172-
Creates a promise to evaluate `expr` when needed with the `force`
173-
primitive. The value of the expression is memoized.
172+
Creates a promise to evaluate `expr` when needed with the `force` primitive.
173+
The value of the expression is memoized so it will only be evaluated once.
174+
175+
Delay internally calls `make-promise` with a thunk that evaluates `expr`.
176+
Delay is efficient, but only safe in the simple case of evaluation in a single thread
177+
of expressions that succeed (i.e. no escaping continuations that later restart).
178+
Using it in the more complex case may cause incorrect or inconsistent behavior.
179+
To support more such complex cases (at a slight extra cost), see `delay-atomic`.
180+
181+
## delay-atomic
182+
```
183+
(delay-atomic expr)
184+
```
185+
186+
Creates a promise to evaluate `expr` when needed with the `force` primitive.
187+
The value of the expression is memoized so it will only be evaluated once.
188+
189+
Delay-atomic internally calls `make-atomic-promise` with a thunk that evaluates `expr`.
190+
Delay-atomic is only slightly less efficient than `delay`, and is safe even in the case of
191+
concurrent evaluation in a multiple threads; it will also support failures and escapes from
192+
the thunk, and will issue an error if someone attempts to reenter such escaped thunks.
174193

175194
## do
176195
TODO
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Control Flow
2+
3+
## make-promise
4+
```
5+
(make-promise thunk) -> promise
6+
7+
thunk := procedure taking no args
8+
```
9+
10+
Creates a promise. You can access the result of the promise with `force`,
11+
which will compute the `thunk` the first time, memoize its result, and subsequently return it always.
12+
13+
Note: The syntax `(delay expr)` creates a promise as if by `(make-promise (lambda () expr))`
14+
15+
Beware: promises returned by `make-promise` are optimized for efficient use in a single-threaded context,
16+
and are not thread-safe, nor safe against escaping continuations that somehow reenter the thunk
17+
before it was fully computed. If you want safety in those cases, use `make-atomic-promise` below.
18+
19+
## promise?
20+
``` scheme
21+
(promise? obj) -> boolean
22+
23+
obj := any object
24+
```
25+
26+
Returns true if the object *obj* is a promise.
27+
28+
## make-atomic-promise
29+
```
30+
(make-atomic-promise thunk) -> promise
31+
32+
thunk := procedure taking no args
33+
```
34+
35+
Creates a promise that is safe in a single-threaded context.
36+
The promise can be fulfilled with the usual `force` primitive, same as with `make-promise`.
37+
38+
It is safe for users in multiple threads to simultaneously try to force the promise:
39+
the first user will do the work, the other ones will wait for its result.
40+
The thunk of an atomic-promise will have only one instance running at once,
41+
and may complete only once, after which it will run no more and the result will be reused instead.
42+
In case of errors and retries from a caller,
43+
the partial side-effects of the incomplete thunk may happen
44+
more than once, so the thunk is responsible for appropriately protecting
45+
any data structure that may be affected, or for ensuring its partial effects are idempotent.
46+
47+
If an error occurs while computing the thunk, or evaluation otherwise escapes from the thunk,
48+
the calling thread may catch the error or escaping value and process it;
49+
but it is an error that will be caught to then try to resume evaluation of the thunk
50+
from a continuation captured within it.
51+
Instead, the promise may be forced again by the same thread or another concurrent thread,
52+
that will hopefully evaluate the thunk to completion and return its result,
53+
or then again may in turn error out and escape.
54+
55+
56+
## call-with-parameters
57+
``` scheme
58+
(call-with-parameters thunk . parameterization) -> any
59+
60+
thunk := procedure taking no args
61+
62+
parameterization:
63+
parameter value ...
64+
```
65+
66+
Calls *thunk* with parameterization.
67+
68+
## with-catch
69+
``` scheme
70+
(with-catch handler thunk) -> any
71+
72+
handler, thunk := procedure
73+
```
74+
75+
Calls *thunk* with *handler* as the exception catcher.
76+
77+
## with-unwind-protect
78+
``` scheme
79+
(with-unwind-protect thunk fini) -> any
80+
81+
thunk, fini := procedure
82+
```
83+
84+
Calls *thunk*, invoking *fini* when execution exits the dynamic extent
85+
of *thunk*.
86+

src/gerbil/runtime/control.ss

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,34 @@ namespace: #f
1212
=> :promise
1313
(:- (##make-delay-promise thunk) :promise))
1414

15+
;; A regular promise is not thread-safe, and there is a race condition in who will force the computation,
16+
;; wherein multiple threads might simultaneously do it, causing side-effects to happen multiple times,
17+
;; with different results, and the second to terminate to overwrite the first results.
18+
;; An atomic promise, by contrast, ensures that the thunk will be computed once and only once.
19+
;;
20+
;; This implementation offers the same API as a regular promise, with the same force primitive,
21+
;; oblivious that more work is done, at the cost of a bit extra overhead in this atomic case.
22+
;; If you care more about performance than about easy expression of safe computation,
23+
;; use lower-level primitives, or go deeper implement this functionality deeper into Gambit.
24+
;; Note that there is indeed a race condition for who will force the inner promise,
25+
;; but all racing workers except the first one will then wait for the mutex,
26+
;; grab the completed inner promise result, and idempotently all set the result of the outer promise
27+
;; to the same result of the inner promise, which is safe.
1528
(def (make-atomic-promise (thunk : :procedure))
1629
=> :promise
17-
(let ((mx (make-mutex 'promise))
18-
(inner (make-promise thunk)))
19-
(make-promise
30+
(let ((inner (make-promise thunk)) ;; inner thread-unsafe promise
31+
(mx (make-mutex 'promise))) ;; mutex ensuring only one worker may force the inner promise
32+
(make-promise ;; outer promise allowing the usual force primitive to work
2033
(lambda ()
21-
(let (once (vector 0))
22-
(dynamic-wind
34+
(let (once (vector 0)) ;; per-worker atomic marker that ensures there is no reentry in code
35+
(dynamic-wind ;; ensure that escaping continuation may not cause reentry into the forcing frame
2336
(lambda ()
2437
(declare (not interrupts-enabled))
25-
(unless (##fx= (##vector-cas! once 0 1 0) 0)
38+
(unless (##fx= (##vector-cas! once 0 1 0) 0) ;; if you try hard to break atomicity, you lose
2639
(error "Cannot reenter atomic block"))
27-
(mutex-lock! mx))
40+
(mutex-lock! mx)) ;; now get the mutex so you're the only one to compute the inner promise
2841
(cut ##force-out-of-line inner)
29-
(cut mutex-unlock! mx)))))))
42+
(cut mutex-unlock! mx))))))) ;; release the mutex
3043

3144
(def* call-with-parameters
3245
(((thunk : :procedure)) (thunk))

0 commit comments

Comments
 (0)