Skip to content

Concurrency ​

Docker. async / await, spawn, all, race, and where a background failure surfaces.

bash
lask sync
time lask run sequential     # two one-second steps, one after the other
time lask run concurrent     # the same two, overlapped
lask eval gather ; lask eval fastest ; lask eval handled

The thing to take away: async starts the work, await joins it, and a failure inside a handle is raised where it is awaited.

To try the commands above, paste the source below into main.lask in an empty directory and run lask sync first, or clone the repository:

bash
git clone https://github.com/lask-task-runner/lask.git
cd lask/example/02-language/06-concurrency

Source ​

This is example/02-language/06-concurrency in the lask repository.

lask
// Concurrency
//
// `async` starts work now and hands back a handle; `await` joins it.
// Needs Docker — the steps below sleep in a container.
//
//   lask sync
//   time lask run sequential     # about 2 seconds
//   time lask run concurrent     # about 1
//   lask eval gather

alpine = #alpine:3.22.2

// One unit of work: sleep, then report.
//
// @param name     What to call this step in the result.
// @param seconds  How long it takes.
step(name: String, --seconds: Number = 1): String = do {
  $[alpine] sleep #{seconds}
  "#{name} done"
}

// Ordinary calls run one after the other.
sequential(): Array<String> = [step("api"), step("web")]

// The same two steps, at the same time.
//
// `async e` starts `e` immediately and evaluates to an
// `AsyncHandle<T>`; `await h` joins it and yields the `T`. Both handles
// are started before either is awaited — that is the whole trick.
concurrent(): Array<String> = do {
  api = async step("api")
  web = async step("web")
  [await api, await web]
}

// Start a set of steps and join all of them.
//
// `async e` is sugar for `spawn(\() -> e)`, so a set of handles can be
// built with `map` and joined at once with `all`, which returns the
// results in input order. If one step fails, `all` fails at once with
// that failure and stops the steps still running.
//
// @return One line per step, in the order given.
gather(): Array<String> = do {
  handles = map(["api", "web", "worker"], \(n: String) -> async step(n))
  all(handles)
}

// Take whichever step finishes first.
//
// `race` returns the first result to arrive — success or failure.
fastest(): String = do {
  slow = async step("slow", seconds = 3)
  fast = async step("fast", seconds = 1)
  race([slow, fast])
}

// Await one handle twice.
//
// Awaiting the same handle twice yields the same result; the work is
// done once.
reused(): Array<String> = do {
  h = async step("once")
  [await h, await h]
}

// Catch a failure that happened in the background.
//
// A failure inside a handle is raised where it is awaited, not where it
// was started — so it is caught around the `await`.
//
// @return The error the background step failed with.
handled(): String = do {
  h = async broken()
  try {
    await h
  } catch (e) {
    "caught #{e.code}: #{trim(e.message)}"
  }
}

internal broken(): String = do {
  $[alpine] sh -c 'echo "the step exploded" 1>&2; exit 7'
  "unreachable"
}

// Start work with `spawn` instead of `async`.
//
// `spawn` is the same thing spelled as a function, taking a
// zero-argument function.
spawned(): String = do {
  h = spawn(\() -> step("spawned"))
  await h
}

Released under the MIT License.