ki: A lightweight, structured concurrency library

This is a package candidate release! Here you can preview how this package release will appear once published to the main package index (which can be accomplished via the 'maintain' link below). Please note that once a package has been published to the main package index it cannot be undone! Please consult the package uploading documentation for more information.

[maintain] [Publish]

A lightweight, structured-concurrency library. This package comes in two variants: * Ki exposes the most stripped-down variant; start here. * Ki.Implicit extends Ki with an implicit context that's used to propagate soft cancellation signals. Using this variant comes at a cost: * You must manually add constraints to propagate the implicit context to where it's needed. * To remain warning-free, you must delete the implicit context constraints where they are no longer needed. If you don't need soft-cancellation, there is no benefit to using this variant, and you should stick with Ki. Because you'll only ever need one variant at a time, I recommend using a mixin stanza to rename one module to Ki while hiding the others. This also simplifies the process of upgrading from Ki.Implicit to Ki if necessary. mixins: ki (Ki.Implicit as Ki)


[Skip to Readme]

Properties

Versions 0.1.0, 0.1.0.1, 0.1.0.1, 0.2.0, 0.2.0.1, 1.0.0, 1.0.0.1, 1.0.0.2, 1.0.1.0, 1.0.1.1, 1.0.1.2
Change log CHANGELOG.md
Dependencies base (>=4.12.0.0 && <4.15), containers, stm [details]
License BSD-3-Clause
Copyright Copyright (C) 2020 Mitchell Rosen
Author Mitchell Rosen
Maintainer Mitchell Rosen <mitchellwrosen@gmail.com>
Category Concurrency
Home page https://github.com/mitchellwrosen/ki
Bug tracker https://github.com/mitchellwrosen/ki/issues
Source repo head: git clone https://github.com/mitchellwrosen/ki.git
Uploaded by mitchellwrosen at 2020-12-01T01:35:32Z

Modules

[Index] [Quick Jump]

Flags

Manual Flags

NameDescriptionDefault
test

Internal flag used by DejaFu test suite

Disabled

Use -f <flag> to enable a flag, or -f -<flag> to disable that flag. More info

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees


Readme for ki-0.1.0.1

[back to package description]

ki

GitHub CI Hackage Stackage LTS Stackage Nightly Dependencies

ki is a lightweight structured-concurrency library inspired by many other projects:

Overview

Structured concurrency

Structured concurrency aims to make concurrent programs easier to understand by delimiting the lifetime of all concurrently threads to a syntactic block, akin to structured programming.

This library defines five primary functions; please read the Haddocks for more comprehensive usage information.

-- Perform an IO action within a new scope
scoped :: (Scope -> IO a) -> IO a

-- Create a background thread (propagates exceptions to its parent)
fork :: Scope -> IO a -> IO (Thread a)

-- Create a background thread (does not propagate exceptions to its parent)
async :: Scope -> IO a -> IO (Either ThreadFailed a)

-- Wait for a thread to finish
await :: Thread a -> IO a

-- Wait for all threads created within a scope to finish
wait :: Scope -> IO ()

A Scope is an explicit data structure from which threads can be created, with the property that by the time the Scope itself "goes out of scope", all threads created within it will have finished.

When viewing a concurrent program as a "call tree" (analogous to a call stack), this approach, in contrast to to directly creating green threads in the style of Haskell's forkIO or Golang's go, respects the basic function abstraction, in that each function has a single ingress and a single egress.

Please read Notes on structured concurrency for a more detailed overview on structured concurrency.

Error propagation

When a parent thread throws or is thrown an exception, it first throws exceptions to all of its children and waits for them to finish. This makes threads hierarchical: a thread cannot outlive the thread that created it.

When a child thread throws or is thrown an exception, depending on how it was created (see fork and async above), it may propagate the exception to its parent. This is intended to cover both of the following cases:

Soft-cancellation

Sometimes it is desirable to inform threads that they should endeavor to complete their work and then gracefully terminate. This is a "cooperative" or "soft" cancellation, in contrast to throwing a thread an exception so that it terminates immediately.

In ki, soft-cancellation is exposed as an alternative superset of the core API, because it involves additional plumbing of an opaque Context type.

withGlobalContext :: (Context => IO a) -> IO a

scoped :: Context => (Context => Scope -> IO a) -> IO a

fork :: Scope -> (Context => IO a) -> IO (Thread a)

Creating a new scope requires a context, whereas the callbacks provided to scoped and fork are provided a context. (Above, the context is passed around as an implicit parameter, but could instead be passed around in a reader monad or similar).

The core API is extended with two functions to soft-cancel a scope, and to observe whether one's own scope has been canceled.

cancelScope :: Scope -> IO ()

cancelled :: Context => IO (Maybe CancelToken)

Canceling a scope is observable by all threads created within it, all threads created within those threads, and so on.

A small soft-cancellation example

A worker thread may be written to perform a task in a loop, and cooperatively check for cancellation before doing work.

worker :: Ki.Context => IO ()
worker =
  forever do
    checkCancellation
    doWork
  where
    checkCancellation :: IO ()
    checkCancellation = do
      maybeCancelToken <- Ki.cancelled
      case maybeCancelToken of
        Nothing -> pure ()
        Just cancelToken -> do
          putStrLn "I'm cancelled! Time to clean up."
          doCleanup
          throwIO cancelToken

The parent of such worker threads may (via some signaling mechanism) determine that it should cancel them, do so, and then defensively fall back to hard-cancelling in case some worker is not respecting the soft-cancel signal, for whatever reason.

Ki.scoped \scope -> do
  worker

  -- Some time later, we decide to soft-cancel
  Ki.cancel scope

  -- Give the workers up to 10 seconds to finish
  Ki.waitFor scope (10 * Ki.seconds)

  -- Fall through the bottom of `scoped`, which throws hard-cancels all
  -- remaining threads by throwing each one an asynchronous exceptions

Testing

(Some of) the implementation is tested for deadlocks, race conditions, and other concurrency anomalies by dejafu, a fantastic unit-testing library for concurrent programs.

Nonetheless this library should not considered production-ready!

In chronological order of publication,