1 # Parallel map and reduce over lists and arrays, a worker pool, select over channels, and timeouts. 2 import Base 3 4 # import bend-kit-concurrency@0.2.0.0/concurrency.bend as Conc 5 # ys = Conc.par_map(~U32, ~U32, ~work, 8, xs) 6 # 7 # par_map and par_reduce are pure. They split the input into at most `workers` 8 # balanced parts (rounded down to a power of two) and run the parts as parallel 9 # calls, so a native build spreads them over its `--threads`. Results keep the 10 # input order. par_reduce needs an associative f; the list form also needs z to 11 # be f's identity, since each part starts from z. 12 # 13 # pool, select, and timeout are IO, on Base's Chan, IO.spawn and IO.fork. Base 14 # has no cancellation: an abandoned computation keeps running, and the program 15 # exits only when every computation is done. 16 17 # The length of xs, beside xs. 18 def count.cons(-A: Type, h: A, r: Nat & List) -> Nat & List: 19 (n, t) = r 20 (1n+n, h <> t) 21 22 def count(-A: Type, xs: List) -> Nat & List: 23 match xs: 24 case Nil{}: 25 (0n, Nil{}) 26 case h <> t: 27 count.cons(A, h, count(A, t)) 28 29 # The first n elements of xs, and the rest. 30 def split.cons(-A: Type, h: A, r: List & List) -> List & List: 31 (l, rest) = r 32 (h <> l, rest) 33 34 def split(-A: Type, xs: List, n: Nat) -> List & List: 35 match xs n: 36 case Nil{} _: 37 (Nil{}, Nil{}) 38 case h <> t 0n: 39 (Nil{}, h <> t) 40 case h <> t 1n+p: 41 split.cons(A, h, split(A, t, p)) 42 43 # lr holds two halves of nl and nr elements. Depth d runs 2^(d+1) parallel parts. 44 def par_map.list.go( 45 ~A: Type, ~B: Type, ~f: A -> B, d: Nat, +nl: Nat, +nr: Nat, lr: List & List 46 ) -> List: 47 match d: 48 case 0n: 49 (l, r) = lr 50 lo hi = List.map(~A, ~B, ~f, l) List.map(~A, ~B, ~f, r) 51 List.append(&1, B, lo, hi) 52 case 1n+ +p: 53 (l, r) = lr 54 +hl = Nat.div(nl, 2n) 55 +hr = Nat.div(nr, 2n) 56 ls = split(A, l, hl) 57 rs = split(A, r, hr) 58 lo hi = par_map.list.go(~A, ~B, ~f, p, hl, Nat.sub(nl, hl), ls) par_map.list.go(~A, ~B, ~f, p, hr, Nat.sub(nr, hr), rs) 59 List.append(&1, B, lo, hi) 60 61 def par_map.list.top(~A: Type, ~B: Type, ~f: A -> B, d: Nat, nx: Nat & List) -> List: 62 match d: 63 case 0n: 64 (n, xs) = nx 65 List.map(~A, ~B, ~f, xs) 66 case 1n+p: 67 (+n, xs) = nx 68 +h = Nat.div(n, 2n) 69 par_map.list.go(~A, ~B, ~f, p, h, Nat.sub(n, h), split(A, xs, h)) 70 71 # List.map(f, xs), on at most `workers` parallel parts. 72 def par_map(~A: Type, ~B: Type, ~f: A -> B, workers: U32, xs: List) -> List: 73 par_map.list.top(~A, ~B, ~f, U32.log2(workers), count(A, xs)) 74 75 def fold(~A: Data, ~f: A -> A -> A, xs: List, acc: A) -> A: 76 match xs: 77 case Nil{}: 78 acc 79 case h <> t: 80 fold(~A, ~f, t, f(acc, h)) 81 82 def par_reduce.list.go( 83 ~A: Data, ~f: A -> A -> A, d: Nat, +nl: Nat, +nr: Nat, +z: A, lr: List & List 84 ) -> A: 85 match d: 86 case 0n: 87 (l, r) = lr 88 lo hi = fold(~A, ~f, l, z) fold(~A, ~f, r, z) 89 f(lo, hi) 90 case 1n+ +p: 91 (l, r) = lr 92 +hl = Nat.div(nl, 2n) 93 +hr = Nat.div(nr, 2n) 94 ls = split(A, l, hl) 95 rs = split(A, r, hr) 96 lo hi = par_reduce.list.go(~A, ~f, p, hl, Nat.sub(nl, hl), z, ls) par_reduce.list.go(~A, ~f, p, hr, Nat.sub(nr, hr), z, rs) 97 f(lo, hi) 98 99 def par_reduce.list.top(~A: Data, ~f: A -> A -> A, d: Nat, +z: A, nx: Nat & List) -> A: 100 match d: 101 case 0n: 102 (n, xs) = nx 103 fold(~A, ~f, xs, z) 104 case 1n+p: 105 (+n, xs) = nx 106 +h = Nat.div(n, 2n) 107 par_reduce.list.go(~A, ~f, p, h, Nat.sub(n, h), z, split(A, xs, h)) 108 109 # The left fold of xs from z, on at most `workers` parallel parts. f must be 110 # associative and z its identity. 111 def par_reduce(~A: Data, ~f: A -> A -> A, workers: U32, +z: A, xs: List) -> A: 112 par_reduce.list.top(~A, ~f, U32.log2(workers), z, count(A, xs)) 113 114 # An Array is a balanced tree, so each ANode above depth d is a parallel call. 115 def par_map.array.go(~A: Type, ~B: Type, ~f: A -> B, a: Array, d: Nat) -> Array: 116 match a d: 117 case ALeaf{x} _: 118 ALeaf{f(x)} 119 case ANode{xs, ys} 0n: 120 ANode{par_map.array.go(~A, ~B, ~f, xs, 0n), par_map.array.go(~A, ~B, ~f, ys, 0n)} 121 case ANode{xs, ys} 1n+ +p: 122 lo hi = par_map.array.go(~A, ~B, ~f, xs, p) par_map.array.go(~A, ~B, ~f, ys, p) 123 ANode{lo, hi} 124 125 # f on every slot of a, on at most `workers` parallel parts. Slots keep their index. 126 def par_map.array(~A: Type, ~B: Type, ~f: A -> B, workers: U32, a: Array) -> Array: 127 par_map.array.go(~A, ~B, ~f, a, U32.log2(workers)) 128 129 def par_reduce.array.go(~A: Type, ~f: A -> A -> A, a: Array, d: Nat) -> A: 130 match a d: 131 case ALeaf{x} _: 132 x 133 case ANode{xs, ys} 0n: 134 f(par_reduce.array.go(~A, ~f, xs, 0n), par_reduce.array.go(~A, ~f, ys, 0n)) 135 case ANode{xs, ys} 1n+ +p: 136 lo hi = par_reduce.array.go(~A, ~f, xs, p) par_reduce.array.go(~A, ~f, ys, p) 137 f(lo, hi) 138 139 # The slots of a combined in index order with an associative f, on at most 140 # `workers` parallel parts. An Array is never empty, so no identity is needed. 141 def par_reduce.array(~A: Type, ~f: A -> A -> A, workers: U32, a: Array) -> A: 142 par_reduce.array.go(~A, ~f, a, U32.log2(workers)) 143 144 # A pool worker between channel steps. It owns its state s alone. 145 type Worker<-S: Type, -A: Type, -B: Type> is Type: 146 WIdle{s: S} 147 WGot{s: S, got: Maybe<&1, A & Chan(B)>} 148 WDone{sb: S & B, reply: Chan(B)} 149 150 # Takes jobs until the queue is closed and empty, then answers its state. 151 # Each job takes three steps, so 3 * (jobs + 1) fuel is enough. 152 def pool.worker( 153 ~S: Type, ~A: Type, ~B: Type, ~work: S -> A -> IO(S & B), fuel: Nat, 154 +q: Chan(A & Chan(B)), w: Worker 155 ) -> IO(S): 156 match fuel: 157 case 0n: 158 IO.die(S, 1, "Conc.pool: a worker ran out of fuel") 159 case 1n+p: 160 match w: 161 case WIdle{s}: 162 do IO: 163 got : Maybe<&1, A & Chan(B)> <- Chan.recv(A & Chan(B), q) 164 pool.worker(~S, ~A, ~B, ~work, p, q, WGot{s, got}) 165 case WGot{s, got}: 166 match got: 167 case None{}: 168 IO.pure(S, s) 169 case Some{(a, reply)}: 170 do IO: 171 sb : S & B <- work(s, a) 172 pool.worker(~S, ~A, ~B, ~work, p, q, WDone{sb, reply}) 173 case WDone{sb, reply}: 174 (s, b) = sb 175 do IO: 176 sent : Result<&1, &1, B, Unit> <- Chan.send(B, reply, b) 177 pool.worker(~S, ~A, ~B, ~work, p, q, WIdle{s}) 178 179 # Queues each job with its own reply channel, and answers the replies in order. 180 def pool.put(~A: Type, ~B: Type, xs: List, +q: Chan(A & Chan(B))) -> IO(List): 181 match xs: 182 case Nil{}: 183 IO.pure(List, Nil{}) 184 case x <> rest: 185 do IO>: 186 reply : Chan(B) <- Chan.new(B, 1) 187 +r : Chan(B) = reply 188 sent : Result<&1, &1, A & Chan(B), Unit> <- Chan.send(A & Chan(B), q, (x, r)) 189 replies : List <- pool.put(~A, ~B, rest, q) 190 return r <> replies 191 192 def pool.start( 193 ~S: Type, ~A: Type, ~B: Type, ~work: S -> A -> IO(S & B), states: List, 194 +fuel: Nat, +q: Chan(A & Chan(B)) 195 ) -> IO(List): 196 match states: 197 case Nil{}: 198 IO.pure(List, Nil{}) 199 case s <> rest: 200 do IO>: 201 c : Chan(S) <- IO.fork(S, pool.worker(~S, ~A, ~B, ~work, fuel, q, WIdle{s})) 202 cs : List <- pool.start(~S, ~A, ~B, ~work, rest, fuel, q) 203 return c <> cs 204 205 def pool.join(-A: Type, cs: List) -> IO(List): 206 match cs: 207 case Nil{}: 208 IO.pure(List, Nil{}) 209 case c <> rest: 210 do IO>: 211 x : A <- IO.join(A, c) 212 xs : List <- pool.join(A, rest) 213 return x <> xs 214 215 def pool.run( 216 ~S: Type, ~A: Type, ~B: Type, ~work: S -> A -> IO(S & B), states: List, 217 nx: Nat & List 218 ) -> IO(List & List): 219 (+n, xs) = nx 220 do IO & List>: 221 queue : Chan(A & Chan(B)) <- Chan.new(A & Chan(B), U32.from_nat(n)) 222 +q : Chan(A & Chan(B)) = queue 223 replies : List <- pool.put(~A, ~B, xs, q) 224 Chan.close(A & Chan(B), q) 225 workers : List <- pool.start(~S, ~A, ~B, ~work, states, Nat.mul(3n, 1n+n), q) 226 outs : List <- pool.join(B, replies) 227 done : List <- pool.join(S, workers) 228 return (done, outs) 229 230 # Runs work(s, x) for every x in xs, on one worker per state in states. A 231 # worker owns its state and threads it through its jobs, so an affine handle 232 # such as a connection pool is never shared. Answers the final states in 233 # worker order and the results in the order of xs. 234 def pool( 235 ~S: Type, ~A: Type, ~B: Type, ~work: S -> A -> IO(S & B), states: List, xs: List 236 ) -> IO(List & List): 237 match states: 238 case Nil{}: 239 IO.die(List & List, 1, "Conc.pool: no workers") 240 case s <> rest: 241 pool.run(~S, ~A, ~B, ~work, s <> rest, count(A, xs)) 242 243 # A select relay between channel steps. 244 type Relay<-A: Type> is Type: 245 RWait{} 246 RGot{got: Maybe<&1, A>} 247 RSent{ok: Result<&1, &1, U32 & A, Unit>} 248 249 # Forwards each value of src to out as (i, value), then reports on done. 250 # It stops when src closes, or when out is closed under it. 251 def select.relay( 252 -A: Type, fuel: Nat, +i: U32, +src: Chan(A), +out: Chan(U32 & A), 253 +done: Chan(Unit), r: Relay 254 ) -> IO(Result<&1, &1, Unit, Unit>): 255 match fuel: 256 case 0n: 257 Chan.send(Unit, done, Unit{}) 258 case 1n+p: 259 match r: 260 case RWait{}: 261 do IO>: 262 got : Maybe<&1, A> <- Chan.recv(A, src) 263 select.relay(A, p, i, src, out, done, RGot{got}) 264 case RGot{got}: 265 match got: 266 case None{}: 267 Chan.send(Unit, done, Unit{}) 268 case Some{x}: 269 do IO>: 270 ok : Result<&1, &1, U32 & A, Unit> <- Chan.send(U32 & A, out, (i, x)) 271 select.relay(A, p, i, src, out, done, RSent{ok}) 272 case RSent{ok}: 273 match ok: 274 case Done{u}: 275 select.relay(A, p, i, src, out, done, RWait{}) 276 case Fail{held}: 277 Chan.send(Unit, done, Unit{}) 278 279 # ponytail: relay fuel is the largest Nat literal, and a value takes three 280 # steps, so a relay stops after about 1.4 billion values. Drop the fuel if 281 # Base gains an unbounded loop. 282 def select.start( 283 -A: Type, chans: List, +i: U32, +out: Chan(U32 & A), +done: Chan(Unit) 284 ) -> IO(Nat): 285 match chans: 286 case Nil{}: 287 IO.pure(Nat, 0n) 288 case src <> rest: 289 do IO: 290 IO.spawn(Result<&1, &1, Unit, Unit>, select.relay(A, 4294967295n, i, src, out, done, RWait{})) 291 n : Nat <- select.start(A, rest, (i + 1 : U32), out, done) 292 return 1n+n 293 294 # Closes out once all n relays have reported. 295 def select.close(-A: Type, n: Nat, +out: Chan(U32 & A), +done: Chan(Unit)) -> IO(Unit): 296 match n: 297 case 0n: 298 do IO: 299 Chan.close(U32 & A, out) 300 Chan.close(Unit, done) 301 case 1n+p: 302 do IO: 303 got : Maybe<&1, Unit> <- Chan.recv(Unit, done) 304 select.close(A, p, out, done) 305 306 def select.go(-A: Type, chans: List, +out: Chan(U32 & A), +done: Chan(Unit)) -> 307 IO(Chan(U32 & A)): 308 do IO: 309 n : Nat <- select.start(A, chans, 0, out, done) 310 IO.spawn(Unit, select.close(A, n, out, done)) 311 return out 312 313 # One channel that carries (i, value) for each value received on chans[i], in 314 # the order they arrive. Chan.recv on it waits for whichever source is ready 315 # first, and answers None once every source is closed. The selector owns the 316 # receiving side of its sources. Close it to stop early: each relay then drops 317 # the one value it holds, if any, and stops. 318 def select(-A: Type, chans: List) -> IO(Chan(U32 & A)): 319 do IO: 320 out : Chan(U32 & A) <- Chan.new(U32 & A, 0) 321 done : Chan(Unit) <- Chan.new(Unit, 0) 322 select.go(A, chans, out, done) 323 324 def timeout.won(-A: Type, +c: Chan(Maybe<&1, A>), got: Maybe<&1, Maybe<&1, A>>) -> 325 IO(Maybe<&1, A>): 326 match got: 327 case None{}: 328 IO.pure(Maybe<&1, A>, None{}) 329 case Some{m}: 330 do IO>: 331 Chan.close(Maybe<&1, A>, c) 332 return m 333 334 # Some{result} if act finishes within ms milliseconds, else None at the 335 # deadline. act is not cancelled: it runs on, its late result is dropped, and 336 # the program waits for it (and for the timer) before it exits. 337 def timeout(-A: Type, ms: U32, act: IO(A)) -> IO(Maybe<&1, A>): 338 do IO>: 339 chan : Chan(Maybe<&1, A>) <- Chan.new(Maybe<&1, A>, 0) 340 +c : Chan(Maybe<&1, A>) = chan 341 IO.spawn(Result<&1, &1, Maybe<&1, A>, Unit>, IO.bind(A, Result<&1, &1, Maybe<&1, A>, Unit>, act, x => Chan.send(Maybe<&1, A>, c, Some{x}))) 342 IO.spawn(Result<&1, &1, Maybe<&1, A>, Unit>, IO.bind(Unit, Result<&1, &1, Maybe<&1, A>, Unit>, IO.sleep(ms), u => Chan.send(Maybe<&1, A>, c, None{}))) 343 got : Maybe<&1, Maybe<&1, A>> <- Chan.recv(Maybe<&1, A>, c) 344 timeout.won(A, c, got)