diff --git a/README.md b/README.md index 922980c..0f8fff5 100644 --- a/README.md +++ b/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) diff --git a/ki.cabal b/ki.cabal index cf27343..5507c24 100644 --- a/ki.cabal +++ b/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 diff --git a/src/Ki.hs b/src/Ki.hs index 2bf3c17..89b33cd 100644 --- a/src/Ki.hs +++ b/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 => -- | diff --git a/src/Ki/Documentation.hs b/src/Ki/Documentation.hs new file mode 100644 index 0000000..b5af878 --- /dev/null +++ b/src/Ki/Documentation.hs @@ -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 +-- @@, 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. diff --git a/src/Ki/Implicit.hs b/src/Ki/Implicit.hs index 989ae53..b47b1be 100644 --- a/src/Ki/Implicit.hs +++ b/src/Ki/Implicit.hs @@ -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 => -- | diff --git a/src/Ki/Internal.hs b/src/Ki/Internal.hs index fa62117..c0b1372 100644 --- a/src/Ki/Internal.hs +++ b/src/Ki/Internal.hs @@ -1,3 +1,4 @@ +-- | Internals. This module does not follow the . module Ki.Internal ( module Ki.Internal.CancelToken, module Ki.Internal.Context, diff --git a/src/Ki/Reader.hs b/src/Ki/Reader.hs index 98cec7c..c887ed1 100644 --- a/src/Ki/Reader.hs +++ b/src/Ki/Reader.hs @@ -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 => -- | diff --git a/src/Ki/Tutorial.hs b/src/Ki/Tutorial.hs deleted file mode 100644 index a82e25c..0000000 --- a/src/Ki/Tutorial.hs +++ /dev/null @@ -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.