mirror of
https://github.com/awkward-squad/ki.git
synced 2024-10-03 22:57:51 +03:00
more documentation work
This commit is contained in:
parent
eb5df7f378
commit
3d29f59aff
124
README.md
124
README.md
@ -15,126 +15,6 @@
|
||||
* [Go Concurrency Patterns: Context](https://blog.golang.org/context)
|
||||
* [.NET 4 Cancellation Framework](https://devblogs.microsoft.com/pfxteam/net-4-cancellation-framework/)
|
||||
|
||||
## 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.
|
||||
|
||||
```haskell
|
||||
-- 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](https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/)
|
||||
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:
|
||||
|
||||
* It is is _unexpected_ for a thread to fail; if it does, the program should crash loudly.
|
||||
* It is _conceivable_ for a thread to fail; if it does, this is not an exceptional circumstance, so should not require
|
||||
installing an exception handler.
|
||||
|
||||
### 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.
|
||||
|
||||
```haskell
|
||||
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.
|
||||
|
||||
```haskell
|
||||
cancel :: 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.
|
||||
|
||||
```haskell
|
||||
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.
|
||||
|
||||
```haskell
|
||||
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
|
||||
```
|
||||
|
||||
## Recommended reading
|
||||
|
||||
In chronological order of publication,
|
||||
@ -146,3 +26,7 @@ In chronological order of publication,
|
||||
* http://250bpm.com/blog:139
|
||||
* http://250bpm.com/blog:146
|
||||
* http://libdill.org/structured-concurrency.html
|
||||
|
||||
## Documentation
|
||||
|
||||
See [Ki.Documentation](https://hackage.haskell.org/package/ki/docs/Ki-Documentation.html)
|
||||
|
11
ki.cabal
11
ki.cabal
@ -16,15 +16,7 @@ version: 0.2.0.1
|
||||
description:
|
||||
A lightweight structured-concurrency library.
|
||||
.
|
||||
This package provides two variants:
|
||||
.
|
||||
* "Ki" exposes a simplified version of the full API that does not include support for soft-cancellation. Thus, none of
|
||||
the functions mention a __context__ type, and you may use any @MonadUnliftIO@-compatible monad, including plain
|
||||
@IO@. If you do not intend to use soft-cancellation, there is no benefit to using the full API. Start here :)
|
||||
.
|
||||
* "Ki.Implicit" and "Ki.Reader" extend "Ki" with a __context__ type that's used to propagate soft-cancellation
|
||||
signals. Because manually threading the right __context__ throughout a program is error-prone boilerplate,
|
||||
package offers two ways of handling it implicitly: using implicit parameters, and using a reader monad.
|
||||
Please read "Ki.Documentation" for an overview of how to use this library.
|
||||
|
||||
extra-source-files:
|
||||
CHANGELOG.md
|
||||
@ -82,6 +74,7 @@ library
|
||||
unliftio-core
|
||||
exposed-modules:
|
||||
Ki,
|
||||
Ki.Documentation,
|
||||
Ki.Implicit,
|
||||
Ki.Internal,
|
||||
Ki.Reader
|
||||
|
21
src/Ki.hs
21
src/Ki.hs
@ -1,3 +1,4 @@
|
||||
-- | Please read "Ki.Documentation" for an overview of how to use this library.
|
||||
module Ki
|
||||
( -- * Scope
|
||||
Scope,
|
||||
@ -6,20 +7,14 @@ module Ki
|
||||
waitSTM,
|
||||
waitFor,
|
||||
|
||||
-- * Creating threads
|
||||
-- * Thread
|
||||
Thread,
|
||||
|
||||
-- ** Fork
|
||||
fork,
|
||||
fork_,
|
||||
forkWithUnmask,
|
||||
forkWithUnmask_,
|
||||
|
||||
-- ** Async
|
||||
async,
|
||||
asyncWithUnmask,
|
||||
|
||||
-- ** Await
|
||||
await,
|
||||
awaitSTM,
|
||||
awaitFor,
|
||||
@ -52,11 +47,9 @@ import Ki.Internal.Thread
|
||||
)
|
||||
import Ki.Internal.Timeout (timeoutSTM)
|
||||
|
||||
-- | Create a child __thread__ within a __scope__.
|
||||
-- | Create a child thread within a scope.
|
||||
--
|
||||
-- /Throws/:
|
||||
--
|
||||
-- * Calls 'error' if the __scope__ is /closed/.
|
||||
-- Reference manual: "Ki.Documentation#reference_manual_async"
|
||||
async ::
|
||||
MonadUnliftIO m =>
|
||||
-- |
|
||||
@ -68,11 +61,9 @@ async =
|
||||
threadAsync
|
||||
{-# INLINE async #-}
|
||||
|
||||
-- | Variant of 'Ki.async' that provides the __thread__ a function that unmasks asynchronous exceptions.
|
||||
-- | Variant of 'Ki.async' that provides the thread a function that unmasks asynchronous exceptions.
|
||||
--
|
||||
-- /Throws/:
|
||||
--
|
||||
-- * Calls 'error' if the __scope__ is /closed/.
|
||||
-- Reference manual: "Ki.Documentation#reference_manual_async"
|
||||
asyncWithUnmask ::
|
||||
MonadUnliftIO m =>
|
||||
-- |
|
||||
|
280
src/Ki/Documentation.hs
Normal file
280
src/Ki/Documentation.hs
Normal file
@ -0,0 +1,280 @@
|
||||
module Ki.Documentation
|
||||
( -- * Introduction
|
||||
-- $introduction
|
||||
|
||||
-- * Tutorial
|
||||
|
||||
-- ** Background
|
||||
-- $tutorial-background
|
||||
|
||||
-- ** Structured concurrency
|
||||
-- $tutorial-structured-concurrency
|
||||
|
||||
-- ** Creating threads
|
||||
-- $tutorial-creating-threads
|
||||
|
||||
-- ** Exception propagation
|
||||
-- $tutorial-exception-propagation
|
||||
|
||||
-- ** Soft-cancellation
|
||||
-- $tutorial-soft-cancellation
|
||||
|
||||
-- * Examples
|
||||
-- $example-has-context-anchor
|
||||
|
||||
-- ** Integrating ki with your application monad
|
||||
-- $example-has-context
|
||||
|
||||
-- * Reference manual
|
||||
-- $reference-manual
|
||||
)
|
||||
where
|
||||
|
||||
-- $introduction
|
||||
--
|
||||
-- This package provides two variants:
|
||||
-- .
|
||||
-- * "Ki" exposes a simplified version of the full API that does not include support for soft-cancellation. Thus, none of
|
||||
-- the functions mention a __context__ type, and you may use any @MonadUnliftIO@-compatible monad, including plain
|
||||
-- @IO@. If you do not intend to use soft-cancellation, there is no benefit to using the full API. Start here :)
|
||||
-- .
|
||||
-- * "Ki.Implicit" and "Ki.Reader" extend "Ki" with a __context__ type that's used to propagate soft-cancellation
|
||||
-- signals. Because manually threading the right __context__ throughout a program is error-prone boilerplate,
|
||||
-- package offers two ways of handling it implicitly: using implicit parameters, and using a reader monad.
|
||||
|
||||
-- $tutorial-background
|
||||
--
|
||||
-- In GHC Haskell, a thread can be created with 'Control.Concurrent.forkIO' at any time, and its lifetime is unrelated
|
||||
-- to the lifetime of its parent: if the child dies, it has no effect whatsoever on the parent, and vice-versa. The
|
||||
-- programmer is responsible for managing the two parallel threads of execution independently.
|
||||
--
|
||||
-- Commonly, though, there is some higher-level structure to this relationship between parent and child, and we'd like
|
||||
-- to codify that relationship in a general way that permits reuse. For example, a child thread can be considered to
|
||||
-- have a return value, which can be retrieved by making a blocking "await" call. This is idea explored in many
|
||||
-- ecosystems, including in the venerable @async@ package.
|
||||
--
|
||||
-- Exception-handling needs some careful thought in this setting, as a parent thread may want to consider the exceptions
|
||||
-- its children may throw, and a child thread may want to consider the exception its parent may throw.
|
||||
--
|
||||
-- Furthermore, in Haskell, there is a distinction between synchronous and asynchronous exceptions. A synchronous
|
||||
-- exception is thrown directly from an @IO@ action that a thread attempts to perform; it is typically used to indicate
|
||||
-- that the @IO@ action, or some sub-action attempted within, failed in some way. Such exceptions are sometimes
|
||||
-- anticipated and caught by the surrounding context, rather than allowed to propagate indefinitely up the call stack.
|
||||
--
|
||||
-- An asynchronous exception is instead delivered to a thread anonymously, and is not related to whatever @IO@ action
|
||||
-- the target thread happened to be performing at that time. It is strongly recommended to always yield to such
|
||||
-- exceptions, no matter how important of a job the target thread is performing, as ignoring them can can easily bring a
|
||||
-- program into a chaotic state wherein one thread remains running while another thread believes it to have terminated.
|
||||
|
||||
-- $tutorial-structured-concurrency
|
||||
--
|
||||
-- "Structured concurrency", in a nutshell, extends some sensible of implementation of parent-child lifetime and
|
||||
-- exception handling semantics with the additional constraint that a child thread cannot outlive the scope in which it
|
||||
-- is created.
|
||||
--
|
||||
-- The primary benefit of this restriction is that even the concurrent parts of an application respect the basic
|
||||
-- function abstraction. While "structured programming" argues that a program becomes significantly easier to understand
|
||||
-- if each function only has a single exit point (that is, the use of @goto@ to jump arbitrarily from one instruction
|
||||
-- to another is avoided entirely), "structured concurrency" makes the exact same argument, only with
|
||||
-- concurrently-running threads in mind.
|
||||
--
|
||||
-- By the time a function returns, all threads it may have created to accomplish its goal are guaranteed to have
|
||||
-- terminated, so whether or not it created any threads is irrelevant to the calling code's basic understanding of the
|
||||
-- control structure of the program. Functions that create "background threads", like functions that may jump elsewhere
|
||||
-- with @goto@, are avoided entirely.
|
||||
|
||||
-- $tutorial-creating-threads
|
||||
--
|
||||
-- In @ki@, the scope in which threads are created is a first-class value. It can only be created by the
|
||||
-- 'Ki.scoped' (cf. _implicit_ 'Ki.Implicit.scoped', _reader_ 'Ki.Reader.scoped') function, which is a "with-style" (or
|
||||
-- "bracket-style") function that accepts a callback, and the scope is only valid for the duration of the callback.
|
||||
--
|
||||
-- A thread can be only created within a scope, because all variants of creating a thread, such as 'Ki.fork' (cf.
|
||||
-- _implicit_ 'Ki.Implicit.fork', _reader_ 'Ki.Reader.fork') and 'Ki.async' (cf. _implicit_ 'Ki.Implicit.async',
|
||||
-- _reader_ 'Ki.Reader.async') accept a scope as an explicit argument. Each thread created within a scope is said to be
|
||||
-- a sibling of the others, distinct from the parent thread which created the scope itself, and related to each other
|
||||
-- only implicitly by the relationship each has to the parent thread.
|
||||
--
|
||||
-- When the callback provided to 'Ki.scoped' returns, the scope becomes "closed", and no new threads can be created
|
||||
-- within it. All remaining threads that were created within the scope are delivered an asynchronous exception, and
|
||||
-- 'Ki.scoped' does not return until all of them have terminated. This satisfies the basic requirement of structured
|
||||
-- concurrency: a thread cannot outlive the scope in which it was created.
|
||||
--
|
||||
-- Here's a simple example, annotated below.
|
||||
--
|
||||
-- @
|
||||
-- __1.__
|
||||
-- (result1, result2) <-
|
||||
-- Ki.'Ki.scoped' \\scope ->
|
||||
-- __2.__
|
||||
-- thread1 <- Ki.'Ki.async' scope worker1
|
||||
-- thread2 <- Ki.'Ki.async' scope worker2
|
||||
-- __3.__
|
||||
-- result1 <- Ki.'Ki.await' thread1
|
||||
-- result2 <- Ki.'Ki.await' thread2
|
||||
-- __4.__
|
||||
-- pure (result1, result2)
|
||||
-- __5.__
|
||||
-- @
|
||||
--
|
||||
-- 1. First, we open a new scope with 'Ki.scoped'. It's only "open" for the duration of the callback we provide.
|
||||
-- 2. Next, we create two worker threads within the scope.
|
||||
-- 3. Next, we wait for both threads to return with either a value or an exception.
|
||||
-- 4. Finally, we reach the end of the callback. The scope is "closed", and all remaining threads are terminated,
|
||||
-- 5. Here, all threads that were created within it scope are guaranteed to have terminated.
|
||||
--
|
||||
-- An explicit scope is a powerful abstraction: although it is indeed an additional argument to pass around as compared
|
||||
-- to simpler thread creation APIs such as 'Control.Concurrent.forkIO' and
|
||||
-- @<https://hackage.haskell.org/package/async/docs/Control-Concurrent-Async.html#v:async async>@, the relationship
|
||||
-- between threads created within a scope can often simply be read off the page.
|
||||
--
|
||||
-- In the example above, we can immediately see that that @worker1@ and @worker2@ are the only threads created within
|
||||
-- the scope; there are no additional lifetimes to consider when attempting to understand the behavior of this program.
|
||||
--
|
||||
-- This would not be the case if the scope was explicitly passed down into @worker1@, for example, which would bestow
|
||||
-- @worker1@ with the ability to create its own siblings, nor would it be the case if the scope was implicitly passed
|
||||
-- around.
|
||||
--
|
||||
-- Passing a scope value around is still an option, and is necessary for certain advanced use cases, as well as
|
||||
-- implementing concurrency abstractions such as worker pools, actors, and supervisors. But be careful - wherever the
|
||||
-- scope goes, so goes the ability to create threads within it!
|
||||
|
||||
-- $tutorial-exception-propagation
|
||||
--
|
||||
-- In @ki@, exception propagation is bi-directional between parent and child threads. We've already discussed one
|
||||
-- circumstance in which a parent throws exceptions to its children: when the callback provided to 'Ki.scoped' returns.
|
||||
-- But this is also the case if the parent terminates abnormally by throwing an exception, or if it is thrown an
|
||||
-- asynchronous exception from another thread. In short, no matter what happens to a parent thread with an open scope,
|
||||
-- the scope will be closed, at which point all remaining child threads are terminated.
|
||||
--
|
||||
-- @
|
||||
-- 'Ki.scoped' \\scope ->
|
||||
-- __1.__
|
||||
-- __2.__
|
||||
-- @
|
||||
--
|
||||
-- 1. It does not matter how many threads are created within here, whether whether the callback itself throws an
|
||||
-- exception, or whether the parent thread is thrown an asynchronous exception...
|
||||
-- 2. ...by the time we get here, all threads created within the scope are guaranteed to have terminated.
|
||||
--
|
||||
-- Sometimes, a child thread may be performing an operation that is expected to sometimes fail; for this case, @ki@
|
||||
-- provides 'Ki.async', which creates a thread that does not propagate any synchronous exceptions to its parent.
|
||||
-- Rather, these exceptions are made available for the parent thread to 'Ki.await' and handle however it wishes.
|
||||
--
|
||||
-- Other times, it is considered very unexpected or erroneous for a child thread to fail; for this case, @ki@ provides
|
||||
-- 'Ki.fork', which creates a thread that immediately propagates any synchronous exception it throws to its
|
||||
-- parent. The intention is to facillitate "failing fast and loud" when there is little or nothing sensible for the
|
||||
-- programmer to do besides propagate the exception up the call tree.
|
||||
--
|
||||
-- In either case, if a child thread is deliviered an asynchronous exception, it is immediately propagated to its
|
||||
-- parent. This is in accordance with exception-handling best practices, which dictate that asynchronous exceptions
|
||||
-- should always be respected, never ignored, and if caught, should always be re-thrown after performing any desired
|
||||
-- cleanup actions.
|
||||
--
|
||||
-- Each child thread can be thought to increases the "surface area" of the parent thread's identity, because any
|
||||
-- asynchronous exception delivered to any child will ultimately be propagated to the parent.
|
||||
|
||||
-- $tutorial-soft-cancellation
|
||||
--
|
||||
-- TODO
|
||||
|
||||
-- $example-has-context-anchor
|
||||
--
|
||||
-- #example_has_context#
|
||||
|
||||
-- $example-has-context
|
||||
--
|
||||
-- You may have an application monad that is defined similar to the following.
|
||||
--
|
||||
-- @
|
||||
-- data Env
|
||||
-- = Env
|
||||
-- { ...
|
||||
-- }
|
||||
--
|
||||
-- newtype App a
|
||||
-- = App { runApp :: Env -> IO a }
|
||||
--
|
||||
-- instance MonadUnliftIO App where ...
|
||||
-- @
|
||||
--
|
||||
-- To use this module, first add one field to your @Env@ type that holds a __context__.
|
||||
--
|
||||
-- @
|
||||
-- data Env
|
||||
-- = Env
|
||||
-- { ...
|
||||
-- , envContext :: 'Ki.Reader.Context'
|
||||
-- , ...
|
||||
-- }
|
||||
-- @
|
||||
--
|
||||
-- Then, write a 'Ki.Reader.HasContext' instance, which is a bit of boilerplate that encapsulates how to get and set
|
||||
-- this field.
|
||||
--
|
||||
-- @
|
||||
-- instance 'Ki.Reader.HasContext' App where
|
||||
-- 'Ki.Reader.askContext' =
|
||||
-- App \\env -> pure (envContext env)
|
||||
--
|
||||
-- 'Ki.Reader.withContext' context action =
|
||||
-- App \\env -> runApp action env{ envContext = context }
|
||||
-- @
|
||||
--
|
||||
-- And finally, when running your monad down to @IO@ in @main@ by providing an initial environment, use
|
||||
-- 'Ki.Reader.globalContext'.
|
||||
--
|
||||
-- @
|
||||
-- main :: IO ()
|
||||
-- main =
|
||||
-- runApp initialEnv action
|
||||
--
|
||||
-- initialEnv :: Env
|
||||
-- initialEnv =
|
||||
-- Env
|
||||
-- { ...
|
||||
-- , envContext = 'Ki.Reader.globalContext'
|
||||
-- , ...
|
||||
-- }
|
||||
--
|
||||
-- action :: App ()
|
||||
-- action =
|
||||
-- ...
|
||||
-- @
|
||||
|
||||
-- $reference-manual
|
||||
--
|
||||
-- This reference manual contains implementation details for all of the major types and functions provided by the "full"
|
||||
-- variant of this library (i.e. "Ki.Implicit" or "Ki.Reader"), which includes support for soft-cancellation.
|
||||
--
|
||||
-- The implementation of the stripped-down "Ki" variant is the same, but all references to cancellation and contexts can
|
||||
-- simply be ignored.
|
||||
--
|
||||
-- ==== 'Ki.Reader.asyncWithUnmask' #reference_manual_async#
|
||||
--
|
||||
-- 'Ki.Reader.asyncWithUnmask' creates a thread within a scope.
|
||||
--
|
||||
-- If the scope is closed, this function calls 'error'. Otherwise, it creates a thread with the same masking state as
|
||||
-- the thread that created it, and provides the thread with an @unmask@ function, which unmasks asynchronous exceptions.
|
||||
--
|
||||
-- The new thread is tracked in a data structure inside the scope, keyed by a monotonically increasing integer, so that
|
||||
-- when the scope is closed, all remaining threads can be thrown an asynchronous exception in the order they were
|
||||
-- created.
|
||||
--
|
||||
-- When the thread terminates, if it terminated with an exception, it first determines whether or not it should
|
||||
-- propagate the exception to its parent.
|
||||
--
|
||||
-- - If the exception is a 'Ki.Internal.ScopeClosing', it is not propagated, as it is assumed to have come from the
|
||||
-- parent thread directly.
|
||||
-- - If the exception is a 'Ki.Internal.CancelToken' that was observable by the thread calling 'Ki.Reader.cancelled',
|
||||
-- it is not propagated, as the parent thread either initiated the cancellation directly, or else some ancestor of
|
||||
-- the parent thread initiated the cancellation (in which case the parent thread could observe the same cancel token
|
||||
-- with 'Ki.Reader.cancelled'); either way, the parent thread could "know about" the cancellation, so it would be
|
||||
-- incorrect to propagate this exception to the parent thread and induce an immediate termination of the thread's
|
||||
-- siblings.
|
||||
-- - If the exception is asynchronous (e.g. a subclass of 'Control.Exception.SomeAsyncException'), it is propagated.
|
||||
-- - Otherwise, the exception is not propagated.
|
||||
--
|
||||
-- The thread's result is then made available via 'Ki.Reader.await', and finally the thread removes itself from the data
|
||||
-- structure in its scope that tracks its existence, because the purpose of the data structure is to track all threads
|
||||
-- still running within a scope, so they can all be terminated when the scope closes.
|
@ -1,6 +1,8 @@
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
|
||||
-- | This module exposes an API that uses an implicit parameter to pass around the __context__ implicitly. If you do not
|
||||
-- | Please read "Ki.Documentation" for an overview of how to use this library.
|
||||
--
|
||||
-- This module exposes an API that uses an implicit parameter to pass around the __context__ implicitly. If you do not
|
||||
-- intend to use soft-cancellation, you may want to use the simpler API exposed by "Ki".
|
||||
module Ki.Implicit
|
||||
( -- * Context
|
||||
@ -15,20 +17,14 @@ module Ki.Implicit
|
||||
waitSTM,
|
||||
waitFor,
|
||||
|
||||
-- * Creating threads
|
||||
-- * Thread
|
||||
Thread,
|
||||
|
||||
-- ** Fork
|
||||
fork,
|
||||
fork_,
|
||||
forkWithUnmask,
|
||||
forkWithUnmask_,
|
||||
|
||||
-- ** Async
|
||||
async,
|
||||
asyncWithUnmask,
|
||||
|
||||
-- ** Await
|
||||
Ki.await,
|
||||
awaitSTM,
|
||||
awaitFor,
|
||||
@ -81,11 +77,9 @@ import Ki.Internal.Timeout (timeoutSTM)
|
||||
type Context =
|
||||
?context :: Ki.Internal.Context.Context
|
||||
|
||||
-- | Create a __thread__ within a __scope__.
|
||||
-- | Create a thread within a scope.
|
||||
--
|
||||
-- /Throws/:
|
||||
--
|
||||
-- * Calls 'error' if the __scope__ is /closed/.
|
||||
-- Reference manual: "Ki.Documentation#reference_manual_async"
|
||||
async ::
|
||||
MonadUnliftIO m =>
|
||||
-- |
|
||||
@ -98,11 +92,9 @@ async scope action =
|
||||
threadAsync scope (with scope action)
|
||||
{-# INLINE async #-}
|
||||
|
||||
-- | Variant of 'Ki.Implicit.async' that provides the __thread__ a function that unmasks asynchronous exceptions.
|
||||
-- | Variant of 'Ki.Implicit.async' that provides the thread a function that unmasks asynchronous exceptions.
|
||||
--
|
||||
-- /Throws/:
|
||||
--
|
||||
-- * Calls 'error' if the __scope__ is /closed/.
|
||||
-- Reference manual: "Ki.Documentation#reference_manual_async"
|
||||
asyncWithUnmask ::
|
||||
MonadUnliftIO m =>
|
||||
-- |
|
||||
|
@ -1,3 +1,4 @@
|
||||
-- | Internals. This module does not follow the <https://pvp.haskell.org/ Haskell Package Versioning Policy>.
|
||||
module Ki.Internal
|
||||
( module Ki.Internal.CancelToken,
|
||||
module Ki.Internal.Context,
|
||||
|
@ -1,8 +1,9 @@
|
||||
-- | This module exposes an API that uses a reader monad to pass around the __context__ implicitly. If you do not intend
|
||||
-- | Please read "Ki.Documentation" for an overview of how to use this library.
|
||||
--
|
||||
-- This module exposes an API that uses a reader monad to pass around the __context__ implicitly. If you do not intend
|
||||
-- to use soft-cancellation, you may want to use the simpler API exposed by "Ki".
|
||||
--
|
||||
-- For an example of how to integrate this library with your reader monad, click @Example@ on the left or scroll down to
|
||||
-- the bottom of this module.
|
||||
-- For an example of how to integrate this library with your reader monad, see "Ki.Documentation#example_has_context".
|
||||
module Ki.Reader
|
||||
( -- * Context
|
||||
Context,
|
||||
@ -17,20 +18,14 @@ module Ki.Reader
|
||||
waitSTM,
|
||||
waitFor,
|
||||
|
||||
-- * Creating threads
|
||||
-- * Thread
|
||||
Thread,
|
||||
|
||||
-- ** Fork
|
||||
fork,
|
||||
fork_,
|
||||
forkWithUnmask,
|
||||
forkWithUnmask_,
|
||||
|
||||
-- ** Async
|
||||
async,
|
||||
asyncWithUnmask,
|
||||
|
||||
-- ** Await
|
||||
Ki.await,
|
||||
awaitSTM,
|
||||
awaitFor,
|
||||
@ -49,9 +44,6 @@ module Ki.Reader
|
||||
seconds,
|
||||
timeoutSTM,
|
||||
sleep,
|
||||
|
||||
-- * Example
|
||||
-- $example
|
||||
)
|
||||
where
|
||||
|
||||
@ -75,66 +67,6 @@ import Ki.Internal.Thread
|
||||
)
|
||||
import Ki.Internal.Timeout (timeoutSTM)
|
||||
|
||||
-- $example
|
||||
--
|
||||
-- You may have an application monad that is defined similar to the following.
|
||||
--
|
||||
-- @
|
||||
-- data Env
|
||||
-- = Env
|
||||
-- { ...
|
||||
-- }
|
||||
--
|
||||
-- newtype App a
|
||||
-- = App { runApp :: Env -> IO a }
|
||||
--
|
||||
-- instance MonadUnliftIO App where ...
|
||||
-- @
|
||||
--
|
||||
-- To use this module, first add one field to your @Env@ type that holds a __context__.
|
||||
--
|
||||
-- @
|
||||
-- data Env
|
||||
-- = Env
|
||||
-- { ...
|
||||
-- , envContext :: 'Ki.Reader.Context'
|
||||
-- , ...
|
||||
-- }
|
||||
-- @
|
||||
--
|
||||
-- Then, write a 'Ki.Reader.HasContext' instance, which is a bit of boilerplate that encapsulates how to get and set
|
||||
-- this field.
|
||||
--
|
||||
-- @
|
||||
-- instance 'Ki.Reader.HasContext' App where
|
||||
-- 'Ki.Reader.askContext' =
|
||||
-- App \\env -> pure (envContext env)
|
||||
--
|
||||
-- 'Ki.Reader.withContext' context action =
|
||||
-- App \\env -> runApp action env{ envContext = context }
|
||||
-- @
|
||||
--
|
||||
-- And finally, when running your monad down to @IO@ in @main@ by providing an initial environment, use
|
||||
-- 'Ki.Reader.globalContext'.
|
||||
--
|
||||
-- @
|
||||
-- main :: IO ()
|
||||
-- main =
|
||||
-- runApp initialEnv action
|
||||
--
|
||||
-- initialEnv :: Env
|
||||
-- initialEnv =
|
||||
-- Env
|
||||
-- { ...
|
||||
-- , envContext = 'Ki.Reader.globalContext'
|
||||
-- , ...
|
||||
-- }
|
||||
--
|
||||
-- action :: App ()
|
||||
-- action =
|
||||
-- ...
|
||||
-- @
|
||||
|
||||
-- | The class of reader monads that contain a __context__ in their environment.
|
||||
class MonadUnliftIO m => HasContext m where
|
||||
-- | Project the __context__ from the environment.
|
||||
@ -143,11 +75,9 @@ class MonadUnliftIO m => HasContext m where
|
||||
-- | Run an @m@ action, replacing its __context__ with the one provided.
|
||||
withContext :: Context -> m a -> m a
|
||||
|
||||
-- | Create a __thread__ within a __scope__.
|
||||
-- | Create a thread within a scope.
|
||||
--
|
||||
-- /Throws/:
|
||||
--
|
||||
-- * Calls 'error' if the __scope__ is /closed/.
|
||||
-- Reference manual: "Ki.Documentation#reference_manual_async"
|
||||
async ::
|
||||
HasContext m =>
|
||||
-- |
|
||||
@ -158,11 +88,9 @@ async ::
|
||||
async scope action =
|
||||
threadAsync scope (with scope action)
|
||||
|
||||
-- | Variant of 'Ki.Reader.async' that provides the __thread__ a function that unmasks asynchronous exceptions.
|
||||
-- | Variant of 'Ki.Reader.async' that provides the thread a function that unmasks asynchronous exceptions.
|
||||
--
|
||||
-- /Throws/:
|
||||
--
|
||||
-- * Calls 'error' if the __scope__ is /closed/.
|
||||
-- Reference manual: "Ki.Documentation#reference_manual_async"
|
||||
asyncWithUnmask ::
|
||||
HasContext m =>
|
||||
-- |
|
||||
|
@ -1,51 +0,0 @@
|
||||
module Ki.Tutorial
|
||||
( -- * Introduction
|
||||
-- $introduction
|
||||
)
|
||||
where
|
||||
|
||||
-- $introduction
|
||||
--
|
||||
-- In GHC Haskell, a background thread can be spawned with "Control.Concurrent.forkIO" at any time, and its lifetime is
|
||||
-- wholly disconnected from the lifetime of its parent: if the child dies, it has no effect whatsoever on the parent,
|
||||
-- and vice-versa. It is hereafter the programmer's responsibility to manage the two parallel threads of execution.
|
||||
--
|
||||
-- Commonly, though, there is some higher-level structure to this relationship between parent and child, and we'd like
|
||||
-- to codify that relationship in a general way that permits reuse. For example, the notion that a child thread has a
|
||||
-- "return value" is an idea explored in many ecosystems and packages, including the venerable @async@ Haskell package,
|
||||
-- wherein a child thread can be "awaited", which is an operation that blocks until the child thread terminates with a
|
||||
-- value or an exception.
|
||||
--
|
||||
-- Here it is supposed, but not required, that whatever parent thread is creating child threads is also awaiting their
|
||||
-- termination.
|
||||
--
|
||||
-- And when a child thread is though to have a "return value", we of course have to reason through how our program
|
||||
-- should behave if that return value never materializes, because the child thread threw (or, in Haskell, was thrown) an
|
||||
-- exception.
|
||||
--
|
||||
-- Sometimes, it might be erroneous for a parent thread to continue running if one of its children has thrown an
|
||||
-- exception, so we might endeavor to eagerly throw an asynchronous exception from child to parent in this case, so the
|
||||
-- exception is noticed in a timely manner.
|
||||
--
|
||||
-- Other times, it would not make sense
|
||||
--
|
||||
--
|
||||
--
|
||||
--
|
||||
--
|
||||
--
|
||||
--
|
||||
-- it would not it would not make sense for a child thread to continue running
|
||||
-- if its parent is no longer alive, so we might endeavor to kill all of a thread's children just before it terminates.
|
||||
--
|
||||
-- These ideas and more have been explored in various Haskell libraries that are built on top of @forkIO@, most notably
|
||||
-- the venerable @async@ library, which has an entire book authored by Simon Marlowe that motivates its design.
|
||||
--
|
||||
-- Structured concurrency takes the idea of a sensible parent-child relationship one step further by requiring _all_
|
||||
-- concurrently running threads to be created within some "scope", such that when the scope exits, or closes, or ceases
|
||||
-- to exist for one reason or another, all threads that were created within it, if any, are terminated. In short, a
|
||||
-- function cannot create
|
||||
--
|
||||
|
||||
-- Structured concurrency, in a nutshell, is a restricted style of programming wherein a thread is prevented from
|
||||
-- outliving some sort of scope in which it was created.
|
Loading…
Reference in New Issue
Block a user