Import Debian changes 20180207-1
[hcoop/debian/mlton.git] / doc / guide / src / ConcurrentMLImplementation.adoc
CommitLineData
7f918cf1
CE
1ConcurrentMLImplementation
2==========================
3
4Here are some notes on MLton's implementation of <:ConcurrentML:>.
5
6Concurrent ML was originally implemented for SML/NJ. It was ported to
7MLton in the summer of 2004. The main difference between the
8implementations is that SML/NJ uses continuations to implement CML
9threads, while MLton uses its underlying <:MLtonThread:thread>
10package. Presently, MLton's threads are a little more heavyweight
11than SML/NJ's continuations, but it's pretty clear that there is some
12fat there that could be trimmed.
13
14The implementation of CML in SML/NJ is built upon the first-class
15continuations of the `SMLofNJ.Cont` module.
16[source,sml]
17----
18type 'a cont
19val callcc: ('a cont -> 'a) -> 'a
20val isolate: ('a -> unit) -> 'a cont
21val throw: 'a cont -> 'a -> 'b
22----
23
24The implementation of CML in MLton is built upon the first-class
25threads of the <:MLtonThread:> module.
26[source,sml]
27----
28type 'a t
29val new: ('a -> unit) -> 'a t
30val prepare: 'a t * 'a -> Runnable.t
31val switch: ('a t -> Runnable.t) -> 'a
32----
33
34The port is relatively straightforward, because CML always throws to a
35continuation at most once. Hence, an "abstract" implementation of
36CML could be built upon first-class one-shot continuations, which map
37equally well to SML/NJ's continuations and MLton's threads.
38
39The "essence" of the port is to transform:
40----
41callcc (fn k => ... throw k' v')
42----
43{empty}to
44----
45switch (fn t => ... prepare (t', v'))
46----
47which suffices for the vast majority of the CML implementation.
48
49There was only one complicated transformation: blocking multiple base
50events. In SML/NJ CML, the representation of base events is given by:
51[source,sml]
52----
53datatype 'a event_status
54 = ENABLED of {prio: int, doFn: unit -> 'a}
55 | BLOCKED of {
56 transId: trans_id ref,
57 cleanUp: unit -> unit,
58 next: unit -> unit
59 } -> 'a
60type 'a base_evt = unit -> 'a event_status
61----
62
63When synchronizing on a set of base events, which are all blocked, we
64must invoke each `BLOCKED` function with the same `transId` and
65`cleanUp` (the `transId` is (checked and) set to `CANCEL` by the
66`cleanUp` function, which is invoked by the first enabled event; this
67"fizzles" every other event in the synchronization group that later
68becomes enabled). However, each `BLOCKED` function is implemented by
69a callcc, so that when the event is enabled, it throws back to the
70point of synchronization. Hence, the next function (which doesn't
71return) is invoked by the `BLOCKED` function to escape the callcc and
72continue in the thread performing the synchronization. In SML/NJ this
73is implemented as follows:
74[source,sml]
75----
76fun ext ([], blockFns) = callcc (fn k => let
77 val throw = throw k
78 val (transId, setFlg) = mkFlg()
79 fun log [] = S.atomicDispatch ()
80 | log (blockFn:: r) =
81 throw (blockFn {
82 transId = transId,
83 cleanUp = setFlg,
84 next = fn () => log r
85 })
86 in
87 log blockFns; error "[log]"
88 end)
89----
90(Note that `S.atomicDispatch` invokes the continuation of the next
91continuation on the ready queue.) This doesn't map well to the MLton
92thread model. Although it follows the
93----
94callcc (fn k => ... throw k v)
95----
96model, the fact that `blockFn` will also attempt to do
97----
98callcc (fn k' => ... next ())
99----
100means that the naive transformation will result in nested `switch`-es.
101
102We need to think a little more about what this code is trying to do.
103Essentially, each `blockFn` wants to capture this continuation, hold
104on to it until the event is enabled, and continue with next; when the
105event is enabled, before invoking the continuation and returning to
106the synchronization point, the `cleanUp` and other event specific
107operations are performed.
108
109To accomplish the same effect in the MLton thread implementation, we
110have the following:
111[source,sml]
112----
113datatype 'a status =
114 ENABLED of {prio: int, doitFn: unit -> 'a}
115 | BLOCKED of {transId: trans_id,
116 cleanUp: unit -> unit,
117 next: unit -> rdy_thread} -> 'a
118
119type 'a base = unit -> 'a status
120
121fun ext ([], blockFns): 'a =
122 S.atomicSwitch
123 (fn (t: 'a S.thread) =>
124 let
125 val (transId, cleanUp) = TransID.mkFlg ()
126 fun log blockFns: S.rdy_thread =
127 case blockFns of
128 [] => S.next ()
129 | blockFn::blockFns =>
130 (S.prep o S.new)
131 (fn _ => fn () =>
132 let
133 val () = S.atomicBegin ()
134 val x = blockFn {transId = transId,
135 cleanUp = cleanUp,
136 next = fn () => log blockFns}
137 in S.switch(fn _ => S.prepVal (t, x))
138 end)
139 in
140 log blockFns
141 end)
142----
143
144To avoid the nested `switch`-es, I run the `blockFn` in it's own
145thread, whose only purpose is to return to the synchronization point.
146This corresponds to the `throw (blockFn {...})` in the SML/NJ
147implementation. I'm worried that this implementation might be a
148little expensive, starting a new thread for each blocked event (when
149there are only multiple blocked events in a synchronization group).
150But, I don't see another way of implementing this behavior in the
151MLton thread model.
152
153Note that another way of thinking about what is going on is to
154consider each `blockFn` as prepending a different set of actions to
155the thread `t`. It might be possible to give a
156`MLton.Thread.unsafePrepend`.
157[source,sml]
158----
159fun unsafePrepend (T r: 'a t, f: 'b -> 'a): 'b t =
160 let
161 val t =
162 case !r of
163 Dead => raise Fail "prepend to a Dead thread"
164 | New g => New (g o f)
165 | Paused (g, t) => Paused (fn h => g (f o h), t)
166 in (* r := Dead; *)
167 T (ref t)
168 end
169----
170I have commented out the `r := Dead`, which would allow multiple
171prepends to the same thread (i.e., not destroying the original thread
172in the process). Of course, only one of the threads could be run: if
173the original thread were in the `Paused` state, then multiple threads
174would share the underlying runtime/primitive thread. Now, this
175matches the "one-shot" nature of CML continuations/threads, but I'm
176not comfortable with extending `MLton.Thread` with such an unsafe
177operation.
178
179Other than this complication with blocking multiple base events, the
180port was quite routine. (As a very pleasant surprise, the CML
181implementation in SML/NJ doesn't use any SML/NJ-isms.) There is a
182slight difference in the way in which critical sections are handled in
183SML/NJ and MLton; since `MLton.Thread.switch` _always_ leaves a
184critical section, it is sometimes necessary to add additional
185`atomicBegin`-s/`atomicEnd`-s to ensure that we remain in a critical
186section after a thread switch.
187
188While looking at virtually every file in the core CML implementation,
189I took the liberty of simplifying things where it seemed possible; in
190terms of style, the implementation is about half-way between Reppy's
191original and MLton's.
192
193Some changes of note:
194
195* `util/` contains all pertinent data-structures: (functional and
196imperative) queues, (functional) priority queues. Hence, it should be
197easier to switch in more efficient or real-time implementations.
198
199* `core-cml/scheduler.sml`: in both implementations, this is where
200most of the interesting action takes place. I've made the connection
201between `MLton.Thread.t`-s and `ThreadId.thread_id`-s more abstract
202than it is in the SML/NJ implementation, and encapsulated all of the
203`MLton.Thread` operations in this module.
204
205* eliminated all of the "by hand" inlining
206
207
208== Future Extensions ==
209
210The CML documentation says the following:
211____
212
213----
214CML.joinEvt: thread_id -> unit event
215----
216
217* `joinEvt tid`
218+
219creates an event value for synchronizing on the termination of the
220thread with the ID tid. There are three ways that a thread may
221terminate: the function that was passed to spawn (or spawnc) may
222return; it may call the exit function, or it may have an uncaught
223exception. Note that `joinEvt` does not distinguish between these
224cases; it also does not become enabled if the named thread deadlocks
225(even if it is garbage collected).
226____
227
228I believe that the `MLton.Finalizable` might be able to relax that
229last restriction. Upon the creation of a `'a Scheduler.thread`, we
230could attach a finalizer to the underlying `'a MLton.Thread.t` that
231enables the `joinEvt` (in the associated `ThreadID.thread_id`) when
232the `'a MLton.Thread.t` becomes unreachable.
233
234I don't know why CML doesn't have
235----
236CML.kill: thread_id -> unit
237----
238which has a fairly simple implementation -- setting a kill flag in the
239`thread_id` and adjusting the scheduler to discard any killed threads
240that it takes off the ready queue. The fairness of the scheduler
241ensures that a killed thread will eventually be discarded. The
242semantics are little murky for blocked threads that are killed,
243though. For example, consider a thread blocked on `SyncVar.mTake mv`
244and a thread blocked on `SyncVar.mGet mv`. If the first thread is
245killed while blocked, and a third thread does `SyncVar.mPut (mv, x)`,
246then we might expect that we'll enable the second thread, and never
247the first. But, when only the ready queue is able to discard killed
248threads, then the `SyncVar.mPut` could enable the first thread
249(putting it on the ready queue, from which it will be discarded) and
250leave the second thread blocked. We could solve this by adjusting the
251`TransID.trans_id types` and the "cleaner" functions to look for both
252canceled transactions and transactions on killed threads.
253
254John Reppy says that <!Cite(MarlowEtAl01)> and <!Cite(FlattFindler04)>
255explain why `CML.kill` would be a bad idea.
256
257Between `CML.timeOutEvt` and `CML.kill`, one could give an efficient
258solution to the recent `comp.lang.ml` post about terminating a
259function that doesn't complete in a given time.
260[source,sml]
261----
262 fun timeOut (f: unit -> 'a, t: Time.time): 'a option =
263 let
264 val iv = SyncVar.iVar ()
265 val tid = CML.spawn (fn () => SyncVar.iPut (iv, f ()))
266 in
267 CML.select
268 [CML.wrap (CML.timeOutEvt t, fn () => (CML.kill tid; NONE)),
269 CML.wrap (SyncVar.iGetEvt iv, fn x => SOME x)]
270 end
271----
272
273
274== Space Safety ==
275
276There are some CML related posts on the MLton mailing list:
277
278* http://www.mlton.org/pipermail/mlton/2004-May/
279
280that discuss concerns that SML/NJ's implementation is not space
281efficient, because multi-shot continuations can be held indefinitely
282on event queues. MLton is better off because of the one-shot nature
283-- when an event enables a thread, all other copies of the thread
284waiting in other event queues get turned into dead threads (of zero
285size).