Skip to content

Commit 68cf1c3

Browse files
authored
Merge pull request mtesseract#2 from mtesseract/ms-refactoring-and-timer-thread-link
Cleaned up and refactored code base
2 parents 21df3e5 + 23debe6 commit 68cf1c3

File tree

6 files changed

+99
-55
lines changed

6 files changed

+99
-55
lines changed

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright Moritz Schulte (c) 2016, 2017
1+
Copyright Moritz Schulte (c) 2016-2017
22

33
All rights reserved.
44

async-timer.cabal

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: async-timer
2-
version: 0.1.3.1
2+
version: 0.1.4.0
33
synopsis: Provides API for timer based execution of IO actions
44
description: This is a lightweight package built on top of the async package
55
providing easy to use periodic timers. This can be used for executing
@@ -9,7 +9,7 @@ license: BSD3
99
license-file: LICENSE
1010
author: Moritz Schulte
1111
maintainer: [email protected]
12-
copyright: 2016, 2017 Moritz Schulte
12+
copyright: (c) 2016-2017 Moritz Schulte
1313
category: Concurrency
1414
build-type: Simple
1515
extra-source-files: README.md
@@ -20,10 +20,12 @@ library
2020
exposed-modules: Control.Concurrent.Async.Timer
2121
, Control.Concurrent.Async.Timer.Unsafe
2222
other-modules: Control.Concurrent.Async.Timer.Internal
23-
build-depends: base >= 4.7 && < 5
23+
build-depends: base >= 4.9.1.0 && < 5
2424
, lifted-base >= 0.2.3.11 && < 0.3
2525
, lifted-async >= 0.9.1.1 && < 0.10
2626
, monad-control >= 1.0.1.0 && < 1.1
27+
, safe-exceptions >= 0.1.5.0 && < 0.2
28+
, transformers-base >= 0.4.4 && < 0.5
2729
default-language: Haskell2010
2830
ghc-options: -Wall -fno-warn-type-defaults
2931

src/Control/Concurrent/Async/Timer.hs

+13-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{-# LANGUAGE FlexibleContexts #-}
22
{-# LANGUAGE MultiParamTypeClasses #-}
3-
{-# LANGUAGE RankNTypes #-}
43

54
module Control.Concurrent.Async.Timer
65
( Timer
@@ -13,21 +12,19 @@ module Control.Concurrent.Async.Timer
1312

1413
import Control.Concurrent.Async.Lifted.Safe
1514
import Control.Concurrent.Async.Timer.Internal
16-
import Control.Concurrent.Lifted
17-
import Control.Monad
15+
import qualified Control.Concurrent.Async.Timer.Unsafe as Unsafe
16+
import Control.Exception.Safe
1817
import Control.Monad.Trans.Control
1918

20-
withAsyncTimer :: forall m b. (MonadBaseControl IO m, Forall (Pure m))
19+
-- | Spawn a timer thread based on the provided timer configuration
20+
-- and then run the provided IO action, which receives the new timer
21+
-- as an argument and call 'timerWait' on it for synchronization. When
22+
-- the provided IO action has terminated, the timer thread will be
23+
-- terminated also.
24+
--
25+
-- This functions requires the contraint @'Forall' ('Pure' m)@, which
26+
-- means that the monad 'm' needs to satisfy @'StM' m a ~ a@ for all
27+
-- 'a'.
28+
withAsyncTimer :: (MonadBaseControl IO m, MonadMask m, Forall (Pure m))
2129
=> TimerConf -> (Timer -> m b) -> m b
22-
withAsyncTimer conf io = do
23-
mVar <- newEmptyMVar
24-
let timer = Timer { timerMVar = mVar }
25-
timerTrigger = void $ tryPutMVar mVar ()
26-
initDelay' = toMicroseconds $ _timerConfInitDelay conf
27-
interval' = toMicroseconds $ _timerConfInterval conf
28-
timerThread = timerLoop (threadDelay initDelay')
29-
(threadDelay interval')
30-
timerTrigger
31-
withAsync timerThread $ const (io timer)
32-
33-
where toMicroseconds x = x * (10 ^ 3)
30+
withAsyncTimer = Unsafe.withAsyncTimer
+51-10
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,77 @@
11
{-# LANGUAGE FlexibleContexts #-}
2+
{-# LANGUAGE LambdaCase #-}
23
{-# LANGUAGE MultiParamTypeClasses #-}
34

4-
module Control.Concurrent.Async.Timer.Internal where
5+
module Control.Concurrent.Async.Timer.Internal
6+
( Timer(..)
7+
, TimerConf(..)
8+
, TimerException(..)
9+
, defaultTimerConf
10+
, timerThread
11+
, timerConfSetInitDelay
12+
, timerConfSetInterval
13+
, timerWait
14+
) where
515

616
import Control.Concurrent.Lifted
17+
import Control.Exception.Safe
718
import Control.Monad
19+
import Control.Monad.Base
820
import Control.Monad.Trans.Control
9-
import Data.Int
1021

11-
-- | Sleep 'dt' milliseconds.
12-
millisleep :: Int64 -> IO ()
13-
millisleep dt = threadDelay (fromIntegral dt * 10 ^ 3)
22+
-- | Timer specific exception; only used for a graceful termination
23+
-- mechanism for timer threads.
24+
data TimerException = TimerEnd deriving (Typeable, Show)
25+
26+
instance Exception TimerException
27+
28+
-- | This is the type of timer handle, which will be provided to the
29+
-- IO action to be executed within 'withAsyncTimer'. The user can use
30+
-- 'timerWait' on this timer to delay execution until the next timer
31+
-- synchronization event.
32+
newtype Timer = Timer { timerMVar :: MVar () }
1433

34+
-- | Type of a timer configuration.
1535
data TimerConf = TimerConf { _timerConfInitDelay :: Int
1636
, _timerConfInterval :: Int }
1737

38+
-- | This exception handler acts on exceptions of type
39+
-- 'TimerException'. What it essentially does is providing a mechanism
40+
-- for graceful termination of timer threads by simply ignoring the
41+
-- TimerEnd exception.
42+
timerHandler :: Monad m => Handler m ()
43+
timerHandler = Handler $ \case
44+
TimerEnd -> return ()
45+
46+
-- | Sleep 'dt' milliseconds.
47+
millisleep :: MonadBase IO m => Int -> m ()
48+
millisleep dt = threadDelay (fromIntegral dt * 10 ^ 3)
49+
50+
-- | Default timer configuration specifies no initial delay and an
51+
-- interval delay of 1s.
1852
defaultTimerConf :: TimerConf
1953
defaultTimerConf = TimerConf { _timerConfInitDelay = 0
2054
, _timerConfInterval = 1000 }
2155

56+
-- | Set the initial delay in the provided timer configuration.
2257
timerConfSetInitDelay :: Int -> TimerConf -> TimerConf
2358
timerConfSetInitDelay n conf = conf { _timerConfInitDelay = n }
2459

60+
-- | Set the interval delay in the provided timer configuration.
2561
timerConfSetInterval :: Int -> TimerConf -> TimerConf
2662
timerConfSetInterval n conf = conf { _timerConfInterval = n }
2763

28-
newtype Timer = Timer { timerMVar :: MVar () }
64+
-- | IO action to be executed within in a timer thread.
65+
timerThread :: (MonadBaseControl IO m, MonadCatch m) => Int -> Int -> MVar () -> m ()
66+
timerThread initDelay intervalDelay syncMVar =
67+
catches (timerLoop initDelay intervalDelay syncMVar) [timerHandler]
2968

30-
timerLoop :: MonadBaseControl IO m => m () -> m () -> m () -> m ()
31-
timerLoop initDelay intervalDelay timerTrigger = do
32-
initDelay
33-
forever $ timerTrigger >> intervalDelay
69+
-- | Timer loop to be executed within in a timer thread.
70+
timerLoop :: (MonadBaseControl IO m) => Int -> Int -> MVar () -> m ()
71+
timerLoop initDelay intervalDelay syncMVar = do
72+
millisleep initDelay
73+
forever $ putMVar syncMVar () >> millisleep intervalDelay
3474

75+
-- | Wait for the next synchronization event on the givem timer.
3576
timerWait :: MonadBaseControl IO m => Timer -> m ()
3677
timerWait = void . takeMVar . timerMVar
+21-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
{-# LANGUAGE FlexibleContexts #-}
2-
{-# LANGUAGE MultiParamTypeClasses #-}
3-
{-# LANGUAGE RankNTypes #-}
42

53
module Control.Concurrent.Async.Timer.Unsafe
64
( Timer
@@ -11,23 +9,31 @@ module Control.Concurrent.Async.Timer.Unsafe
119
, timerWait
1210
) where
1311

14-
import qualified Control.Concurrent.Async.Lifted as Unsafe
12+
import Control.Concurrent.Async.Lifted
1513
import Control.Concurrent.Async.Timer.Internal
1614
import Control.Concurrent.Lifted
17-
import Control.Monad
15+
import Control.Exception.Safe
1816
import Control.Monad.Trans.Control
1917

20-
withAsyncTimer :: forall m b. (MonadBaseControl IO m)
18+
-- | Spawn a timer thread based on the provided timer configuration
19+
-- and then run the provided IO action, which receives the new timer
20+
-- as an argument and call 'timerWait' on it for synchronization. When
21+
-- the provided IO action has terminated, the timer thread will be
22+
-- terminated also.
23+
withAsyncTimer :: (MonadBaseControl IO m, MonadMask m)
2124
=> TimerConf -> (Timer -> m b) -> m b
2225
withAsyncTimer conf io = do
26+
-- This MVar will be our synchronization mechanism.
2327
mVar <- newEmptyMVar
24-
let timer = Timer { timerMVar = mVar }
25-
timerTrigger = void $ tryPutMVar mVar ()
26-
initDelay' = toMicroseconds $ _timerConfInitDelay conf
27-
interval' = toMicroseconds $ _timerConfInterval conf
28-
timerThread = timerLoop (threadDelay initDelay')
29-
(threadDelay interval')
30-
timerTrigger
31-
Unsafe.withAsync timerThread $ const (io timer)
32-
33-
where toMicroseconds x = x * (10 ^ 3)
28+
let timer = Timer { timerMVar = mVar }
29+
initDelay = _timerConfInitDelay conf
30+
intervalDelay = _timerConfInterval conf
31+
withAsync (timerThread initDelay intervalDelay mVar) $ \asyncHandle -> do
32+
-- This guarantees that we will be informed right away if our
33+
-- timer thread disappears, for example because of an async
34+
-- exception:
35+
link asyncHandle
36+
-- This guarantees that we will throw the TimerEnd exception to
37+
-- the timer thread after the provided IO action has ended
38+
-- (w/ or w/o an exception):
39+
io timer `finally` cancelWith asyncHandle TimerEnd

test/Spec.hs

+8-10
Original file line numberDiff line numberDiff line change
@@ -34,36 +34,34 @@ test1 :: IO ()
3434
test1 = do
3535
let conf = defaultTimerConf & timerConfSetInitDelay 0
3636
& timerConfSetInterval 1000 -- ms
37+
noOfTicks = 5
3738

3839
counter <- newIORef 0
3940
times <- newIORef []
4041

41-
withAsyncTimer conf $ \ timer -> do
42-
forM_ [1..10] $ \_ -> do
42+
withAsyncTimer conf $ \ timer ->
43+
forM_ [1..noOfTicks] $ \_ -> do
4344
timerWait timer
4445
void $ forkIO $ myAction counter times
4546

46-
threadDelay 1000
47+
threadDelay (5 * 10^5)
4748
n <- readIORef counter
48-
n @?= 10
49+
n @?= noOfTicks
4950

5051
ts <- readIORef times
5152
let deltas = case ts of
5253
[] -> []
53-
_ : tsTail -> map (\ (a, b) -> a - b) $ zip ts tsTail
54+
_ : tsTail -> zipWith (-) ts tsTail
5455

55-
diff = sum deltas - 9
56+
avgDiff = sum (map (subtract 1) deltas) / fromIntegral (length deltas)
5657
forM_ deltas (\ dt -> putStrLn $ "dt = " ++ show dt)
57-
putStrLn $ "average dt = " ++ show diff
58+
putStrLn $ "average dt = " ++ show avgDiff
5859
return ()
5960

6061
where myAction :: IORef Int -> IORef [Double] -> IO ()
6162
myAction counter times = do
6263
t <- getTime
6364
n <- readIORef counter
64-
if n == 10
65-
then throwIO MyException
66-
else return ()
6765
let n' = n + 1
6866
writeIORef counter n'
6967
modifyIORef times (t :)

0 commit comments

Comments
 (0)