Features Download
From: Robert Dockins <robdockins <at> fastmail.fm>
Subject: Yet another top-level IO proposal
Newsgroups: gmane.comp.lang.haskell.cafe
Date: Tuesday 17th January 2006 03:14:13 UTC (over 12 years ago)
Fellow haskellers,

I have a proposal I would like to enter into the eternal top-level IO  
debate.  The proposal
involves a minor language extension and some runtime support for  
thread local reference cells.
I believe it has the potential to meet many of the needs of those  
requesting top level IO.

My apologies for this rather lengthy message, but given the  
volatility of discussion on this matter
in the past, it seemed best to lay out my thoughts as precisely as  
possible at the beginning.

So without further ado, the proposal.

The language extension:

-- Add a new keyword 'threadlocal' and a top level declaration with  
the syntax


-- The declaration consists of three parts 1) the name of the  
reference cell 2) the type of
     data stored in the cell and 3) an initializer action.
-- The name of the cell declared with 'threadlocal' shares the  
function namespace and introduces
     a CAF with the type 'TLRef a', where 'a' stands for the type  
given in the declaration.
-- The initializer action must be an expression with type 'TL a',  
where 'a' stands for the
     the type given in the 'threadlocal' declaration, and 'TL' is the  
thread-local initialization
     monad (a little like the ACIO monad, more on this below).

The semantics:

-- Each thread in a program (the main thread and threads sparked from  
forkIO and forkOS) has a "bank"
     of thread local variables.  Writes to and reads from a thread- 
local cell are only written to/read
     from the bank of the thread performing the write/read.
-- For any given bank, a thread-local cell may be "empty" (which  
means it holds no value) or "full"
     with a value of its declared type.
-- There is a phantom bank of thread-local values belonging to no  
thread in which the value of all
     thread-local cells is "empty".  This represents the state of  
thread local variables before program
-- Whenever a thread is sparked (including the main thread) and  
before it begins executing, its
     thread-local variables are initialized.  For each declared  
thread-local variable (in the transitive
     closure of imported modules), the declared initilzation action  
is run and the generated value initializes
     the thread-local cell for that thread.  The initializer actions  
are run in an unspecified order.
-- The primitives of the TL are strictly limited and include only  
actions which have no observable
     side effects (a proposed list of primitives is listed below).  A  
TL action may read from (but NOT write
     to) thread-local cells in the bank of the sparking thread (the  
bank of the thread calling forkIO, or the
     special phantom bank for the main thread).
-- Any exceptions generated during a thread-local initilization  
action are propigated to the thread
     which called forkIO/forkOS or, in the case of the main thread,  
directly to the runtime system just
     as though an uncaught exception bubbled off the main thread.
-- New IO primitives are added to read from, write to and clear (set  
to empty) thread-local variables.


This proposal seems to hit most of the use cases that I recall having  
seen (including the very important
allocate-a-top-level-concurrency-variable use case) and seems to  
provide a nice way to reinterpret some
of the "magic" currently in the standard libraries.  In addition,  
this proposal does not suffer from the
module loading order problem that some previous proposals have;  
because thread local initializer actions
depend only on the "previous" bank of values, the order in which they  
are run makes very little difference
(only for the primitives that read clock time or some such).  The  
value of a thread-local cell is always well-defined,
even before the main thread starts. Values in a thread-local have a  
well defined lifetime that is tied to the owning
thread.  I think that efficient implementation is possible (maybe we  
can play some copy-on-write games?).

I especially like that variables are only as "global" as desired for  
any given task; if a library writer
uses thread-locals for some manner of shared "global" state, later  
users are always able to write programs
that use more than one instance of the "global" state without needing  
to alter the library.


Requires a language extension (but I don't know of a serious  
alternate proposal that doesn't).  Requires
non-trivial runtime system support.  Not sure what effect this has on  
garbage collection. Adds overhead to
thread creation (this could perhaps be mitigated by introducing new  
primitives that distinguish heavyweight
threads with their own thread-local banks from lightweight threads,  
which do not have separate thread-local banks).
Its a bit complicated.  You can shoot yourself in the foot (true of  
most of the other proposals).

Some representative use cases:

-- Implicit parameter style use case:
       You want to provide a default value that you expect will be  
rarely changed.  Threading the
       parameter deeply through the code obsfucates meaning and the  
code is in the IO monad.

       * Define a thread-local variable for the value and set the  
initializer to set the variable to
         some default value if it was empty, or to copy the parent  
thread's value otherwise.
       * If desired, change the variable's value early in main  
(before any other threads are sparked);
         the new value will be propagated to all new threads and be  
available in main.
       * If desired, different threads can set different values of  
the parameter which will then
         be propagated to their sub-threads.

-- Top level synchronization variable use case:
       You need an MVar to manage some "global" resource.

       * Define a thread-local variable to hold the MVar.  Define the  
initializer to create a new MVar
         if the TLRef was empty, and to copy the parent thread's  
value otherwise.
       * Read the MVar from the thread-local var, and use it as usual.
       * Also, allows you to partition the program into sandboxes  
which use distinct MVars to manage
         distinct pools of the "global" resource without needing to  
change or complicate any underlying

-- Running time statistics use case:
       You want to easily keep track of how long each thread in a  
program has been running in wall-clock time.

       * Define a thread-local variable with an initializer that  
reads the current wall-clock time.
       * Calculate the thread running time by taking the difference  
between the current wall-clock time
         and the time in the thread-local var.

-- Give better semantics to standard handles use case:
       You want to make the handling of stdin, stdout and stderr in  
System.IO less "magic" and baked-in.

       * Define a thread-local variable for each of stdin, stdout,  
and stderr.  The initializer action
         creates appropriate handles for each one in the case that  
the thread-local was empty, and
         copies the parent's value otherwise.
       * Nice feature: allows you to override the values of stdin,  
stdout and stderr for sub-threads,
         shell style.

   -- Give better semantics to getArgs use case:
       You want getArgs/withArgs and getProgName/withProgName to have  
better semantics.

       * Define a thread-local variable for the list of arguments and  
for the program name.  Define an
         initializer which reads these values from C land when the  
thread-local value is empty and copies
         the parent thread's value otherwise.
       * Allows you to spark multiple threads in a single program  
with different "command line" arguments.

The proposed list of new primitives:

   -- thread-locals maintained by the standard libs
     currentWorkingDirectory :: TLRef FilePath
     stdin :: TLRef Handle
     stdout :: TLRef Handle
     stderr :: TLRef Handle

   -- in the TL monad

    readTL :: TLRef a -> TL a   -- ^ Reads a thread-local variable in  
the bank of the parent thread.
                                --   Returns bottom if the cell is  
    tryReadTL :: TLRef a -> TL (Maybe a)
                                -- ^ Reads a thread-local variable in  
the bank of the parent thread.
                                --   Returns Nothing if the cell is  
    getClocktimeTL :: TL ClockTime
    getCPUTimeTL :: TL Integer
    getDefaultStdinHandle :: TL Handle
    getDefaultStdoutHandle :: TL Handle
    getDefaultStderrHandle :: TL Handle
    getDefaultArgs :: TL [String]
    getDefaultProgramName :: TL String
    newIORefTL :: a -> TL (IORef a)
    newMVarTL :: a -> TL (MVar a)
    newEmptyMVarTL :: TL (MVar a)
    newSTRefTL :: a -> TL (STRef a)

   -- in the IO monad

    readThreadLocal :: TLRef a -> IO a              -- ^ bottom on empty
    tryReadThreadLocal :: TLRef a -> IO (Maybe a)   -- ^ Nothing on  
    writeThreadLocal :: a -> TLRef a -> IO ()
    clearThreadLocal :: TLRef a -> IO ()            -- ^ Reset the  
thread-local to empty
    clearBank :: IO ()                              -- ^ Clear all  
thread-local cells in the current thread


In order to get discussion flowing and experiment with the semantics  
I have put together
a demonstration module which implements thread-local variables using  
the standard
"unsafePerformIO" hacks.  The module and an example usage are attached.

If there is any interest in these ideas, I will post this proposal to  
the wiki.

Please respond with thoughts and comments,

Rob Dockins

Speak softly and drive a Sherman tank.
Laugh hard; it's a long way to the bank.
           -- TMBG
CD: 49ms