Fiber package reference (V0.2)

::fiber

Introduction

This package provides an implementation of "fibers" for Tcl. Fibers, as the term is used in this package, are threads of execution that are co-operatively multitasked and use message passing as their primary means of communication, including channel based I/O.

Fibers are implemented using Tcl coroutines and are compatible with any Tcl application model provided it runs the Tcl event loop.

Erlang programmers may find some similarity to that language's process model. That is not just happenstance.

Downloading

The package is available from the Mercurial source repository at http://sourceforge.net/projects/tclfiber.

Creating a fiber

The fiber command creates a new fiber and returns its name. This name is used when sending messages to the fiber or identifying messages received by it. The code to be run is passed as a lambda term in the same format as used by Tcl's [apply] command. The fiber terminates when the script returns.

  fiber::fiber logger* {
      {chan}
      {
          while {1} {
              fiber::receive msg type {
                  quit {break}
                  default {puts $chan $msg(sender):$msg(content)}
              }
          }
      }
  }

The above creates a fiber that keeps looping and printing messages until it sees a message of type quit at which point the loop and the fiber terminate.

Fiber execution

Fibers execute in co-operative fashion. The run command must be invoked for fibers to be dispatched. The dispatch loop will execute every fiber in turn that is not blocked. The fiber will run until it explicitly gives up control, for example waiting for a message or channel input.

A script running in a fiber will generally follow one of two patterns:

  • the message loop where the fiber continuously loops receiving and dispatching messages as in the simple example above. The message reception calls permit other fibers to run.
  • the sequential program where the fiber is written primarily as a linear program. Even in this case, calls to retrieve messages and channel input can cause the fiber to give up the processor to other fibers.

Fiber termination

The script running in a fiber may terminate normally or abort via an uncaught exception, or be explicitly terminated with the kill command. The kill command is also delivered as an exception - refer to the command description for the exception format. It is not recommended that this exception be caught to prevent the fiber from dying. Use the mechanisms described in the next section instead.

Aborts via uncaught exceptions are notified by the fiber dispatcher as background errors through the [interp] [bgerror] mechanism.

Linking fibers

Fibers can be linked together via the link command such that each is notified of the termination of the other. The notification is either delivered as a Tcl exception or a message. See the link command for details.

Messaging

Messages passed between fibers are lists with four elements:

  • the sender of the message (often the name of the sending fiber)
  • the intended receiver (the name of the target fiber)
  • the message type
  • the message content

By default, the sender element is the name of the fiber if the message is sent by a fiber (note the sender of a message does not have to be a fiber). It can also be explicitly specified. The receiver is the name of the receiving fiber. The message type field may be any application defined string or a system message type. The latter are identified by being enclosed in <>. The content field is the actual data sent by the sender.

A fiber may receive the following system message types:

<error> signifies an error
<channel_error> error on a registered channel
<line> content is a line received on a channel in line mode
<stream> content is a data block received on a channel in stream mode
<timeout> a time out occured when waiting for a message

Internally, the package uses other system message types as well. The <null> type is a special type that is never sent. It has special uses internally.

Messages are sent using the send command which places the message at the end of the receiver's message queue.

Messages can be received with either the get or the receive commands. Both behave similarly and differ mostly in syntax. The former simply retrieves a message while the latter behaves like a built-in switch statement and can run scripts based on message fields.

Both commands allow for selective receiving based on the sender and message type and can also be used with timeouts.

Input / Output

The fiber package also provides a means for channel input to be received in the form of messages. This has several benefits:

  • it allows a single receive statement to wait for either messages or input on a channel
  • the fiber can be written in a sequential manner reading data in synchronously while not blocking the rest of the application from running.

Only channel input is converted to messages. Channel output is not provided for and is done in the usual fashion as there is no advantage in using messages for output.

See the channel_register and channel_unregister commands for details.

Tcl and Tk event integration

Other than requiring the event loop to be running, the fiber package does not interact with Tcl or Tk events, for example, widget bindings. If an application wants to process widget interaction through the message queue, it can bind the callback to use send to send a message to the appropriate fiber using any suitable message type and content format.

Introspection

The fibers command provides introspection capabilities to view the state of fibers including stack traces for suspended fibers.

Commands

barf [::fiber]

fiber, Top

Generate an error that appears to come from the caller's invocation

barf errorcode msg

Parameters
errorcode value to use for the $::errorCode in generated error
msg(optional, default ) error message. If empty, the last element of errorcode is used
Description

Generate an error that appears to come from the caller's invocation

proc ::fiber::barf {errorcode {msg {}}} {

    # Generate an error that appears to come from the caller's invocation
    #  errorcode - value to use for the $::errorCode in generated error
    #  msg - error message. If empty, the last element of $errorcode is used
    if {$msg eq ""} {
        set msg [lindex $errorcode end]
    }
    return -level 2 -code error -errorcode $errorcode $msg
}

channel_register [::fiber]

fiber, Top

Register a channel for receiving channel input as messages

channel_register chan mode args

Parameters
chan the channel from which data is to be read
mode a list the first element of which must be line or stream followed optionally by autoclose
args any additional options are passed to the channel's configure command
Description

Register a channel for receiving channel input as messages

Registers a channel with the fiber package. The channel is configured to non-blocking mode (note this affects output also) and data read from the channel is delivered to the fiber as messages.

Registered channels are automatically unregistered when the fiber terminates either normally or because of an exception. Alternatively they can be unregistered through the channel_unregister command.

Messages from registered channels always have the channel name in the sender field of the message. The message type may be <line> and <stream> for data messages, <eof> when the channel reaches end-of-file condition, and <channel_error> in case of channel errors.

The first element of mode determines whether the channel operates in line mode or stream mode:

  • in line mode, complete lines are read from the channel and delivered to the fiber as messages of type <line>
  • in stream mode, data is read from the channel in arbitrary sized block and delived as messages of type <stream>

If the second element of mode is auto_close, the channel is closed when channel_unregister is called.

proc ::fiber::channel_register {chan mode args} {

    # Register a channel for receiving channel input as messages
    #     chan - the channel from which data is to be read
    #     mode - a list the first element of which must be 'line' or 'stream'
    #        followed optionally by 'autoclose'
    #     args - any additional options are passed to the channel's
    #       configure command
    # Registers a channel with the fiber package. The channel is configured
    # to non-blocking mode (note this affects output also)
    # and data read from the channel is delivered to the fiber
    # as messages.
    #
    # Registered channels are automatically unregistered when
    # the fiber terminates either normally or because of an exception.
    # Alternatively they can be unregistered through the [channel_unregister]
    # command.
    #
    # Messages from registered channels always have the channel name
    # in the sender field of the message. The message type may be '<line>'
    # and '<stream>' for data messages, '<eof>' when the channel
    # reaches end-of-file condition, and '<channel_error>' in case
    # of channel errors.

    variable channels
    variable fibers

    set me [me]

    if {[dict exists $channels $chan]} {
        barf {FIBER REGISTERED {channel already registered}} "Fiber $me attempted to register channel registered by [dict get $channels $chan fiber]"
    }

    #ruff
    # The first element of $mode determines whether the channel operates
    # in line mode or stream mode:
    #    - in 'line' mode, complete lines are read from the channel
    #      and delivered to the fiber as messages of type '<line>'
    #    - in 'stream' mode, data is read from the channel in arbitrary
    #      sized block and delived as messages of type '<stream>'
    switch -exact -- [lindex $mode 0] {
        line    { set callback [namespace current]::_channel_line_handler }
        stream  { set callback [namespace current]::_channel_stream_handler }
        default {
            barf [list FIBER BADARG "Invalid channel mode '[lindex $mode 0]'. Must be 'line' or 'stream'"]
        }
    }

    dict set channels $chan [list fiber $me mode [lindex $mode 0] original_block_mode [chan configure $chan -blocking]]
    dict set fibers($me) channels $chan {}; # Value {} not used

    #ruff
    # If the second element of $mode is 'auto_close', the channel is
    # closed when [channel_unregister] is called.
    switch [lindex $mode 1] {
        ""         { dict set channels $chan auto_close 0 }
        auto_close { dict set channels $chan auto_close 1 }
        default { barf [list FIBER BADARG "Invalid channel mode option '[lindex $mode 1]'"] }
    }

    if {[catch {
        chan configure $chan -blocking 0 {*}$args
        chan event $chan readable [list $callback $chan]
    } msg options]} {
        channel_unregister $chan
        return -options $options $msg
    }

    return
}

channel_unregister [::fiber]

fiber, Top

Unregister a previously registered channel from its fiber

channel_unregister chan

Parameters
chan the channel to unregister
Description

Unregister a previously registered channel from its fiber

If the channel was registered with the auto_close option the channel is closed when it is unregistered.

proc ::fiber::channel_unregister {chan} {

    # Unregister a previously registered channel from its fiber
    #    chan - the channel to unregister

    variable channels
    variable fibers

    # Note - do not add a check here for whether we are in a fiber
    # as we may be unregistering from a channel handler as well in
    # case of errors

    if {[dict exists $channels $chan]} {
        set chaninfo [dict get $channels $chan]
        dict unset fibers([dict get $channels $chan fiber]) channels $chan
        dict unset channels $chan; # Get rid our bookeeping first
        # The catch is in case app is already closed it behind out backs
        catch {
            #ruff
            # If the channel was registered with the 'auto_close' option
            # the channel is closed when it is unregistered.
            if {[dict get $chaninfo auto_close]} {
                chan close $chan
            } else {
                chan event $chan readable {}
                chan configure $chan -blocking [dict get $chaninfo original_block_mode]
            }
        }
    }
}

fiber [::fiber]

fiber, Top

Creates a new fiber

fiber name lambda args

Parameters
name name to be assigned to the fiber
lambda a lambda term to be run in the fiber. This is in the same format as used in Tcl [apply] command
args arguments to pass to the lambda
Return value

Returns the name of the fiber.

Description

Creates a new fiber

The new fiber is created as a coroutine and lambda is executed within it with args passed as arguments.

The name assigned to the fiber should consist of only of alphanumeric characters and underscores. If the name ends in a '*', the '*' is replaced by a string that ensures uniqueness of the name.

The script should take care not to block for long periods of time as that will prevent other fibers from running. For example, I/O should be done in asynchronous mode with control yielded to the Tcl event loop. If a synchronous style I/O model is desired for ease of programming, the coroutine package from tcllib may be used which implements a synchronous I/O api on top of a asynchronous model.

A common use for fibers is in message passing mode where the script can receive messages sent to them via the receive command. This command will also yield to other fibers if no messages are currently available for the calling fiber.

The fiber is terminated when the script returns. The return value from the script is discarded. The fiber may also be terminated from other fibers or other parts of the application through the kill command. This is not a hard termination and can be caught by the script. See the kill command for more details.

proc ::fiber::fiber {name lambda args} {

    # Creates a new fiber
    #  name - name to be assigned to the fiber
    #  lambda - a lambda term to be run in the fiber. This is in the
    #     same format as used in Tcl [apply] command
    #  args - arguments to pass to the lambda
    #
    # The new fiber is created as a coroutine and $lambda is executed
    # within it with $args passed as arguments.
    #
    # The name assigned to the fiber should consist of only of alphanumeric
    # characters and underscores. If the name ends in a '*',
    # the '*' is replaced by a string that ensures uniqueness of the name.
    #
    # The script should take care not to block for long periods of time
    # as that will prevent other fibers from running. For example, I/O should
    # be done in asynchronous mode with control yielded to the Tcl event loop.
    # If a synchronous style I/O model is desired for ease of programming,
    # the coroutine package from tcllib may be used which
    # implements a synchronous I/O api on top of a asynchronous model.
    #
    # A common use for fibers is in message passing mode where the script
    # can receive messages sent to them via the receive command. This command
    # will also yield to other fibers if no messages are currently available
    # for the calling fiber.
    #
    # The fiber is terminated when the script returns. The return value
    # from the script is discarded. The fiber may also be terminated from
    # other fibers or other parts of the application through the kill command.
    # This is not a hard termination and can be caught by the script.
    # See the kill command for more details.
    #
    # Returns the name of the fiber.

    variable fibers
    variable ready
    variable ref_ctr

    # For time being, until we figure out syntax for distributed fibers
    # restrict chars in the name. For some reason this regexp is marginally
    # faster than [.:\\/@].
    if {[regexp {\.|:|\\|/|@} $name]} {
        barf [list FIBER RESERVEDNAME "Invalid fiber name '$name'"]
    }

    # Only replace the first *, if present, with a counter
    if {[string index $name end] eq "*"} {
        set new_name "[string range $name 0 end-1][incr ref_ctr]"
        while {[info exists fibers($new_name)]} {
            set new_name "[string range $name 0 end-1][incr ref_ctr]"
        }
        set name $new_name
    } else {
        if {[info exists fibers($name)]} {
            barf [list FIBER EXISTS "There is an existing fiber with name '$name'"]
        }
    }

    # TBD - This method is 10% faster but need to check if it will
    # accept all valid code. Note putting the _init inside the try
    # slows it down.
    fiber::_init $name ::fiber::coros::$name
    coroutine ::fiber::coros::$name try [list apply $lambda {*}$args] on error {errmsg erropts} {fiber::_set_exit_state $errmsg $erropts} finally "fiber::_cleanup $name"

    return $name
}

fibers [::fiber]

fiber, Top

Returns a dictionary containing information about currently running fibers

fibers level

Parameters
level(optional, default summary)
Return value

Returns a dictionary containing information about currently running fibers

Description
level controls how much information is returned

The returned dictionary is indexed by the fiber name with the corresponding value itself being a dictionary with the following keys:

message_count count of messages pending for the fiber
state the current state of the fiber

If level is detail, the following additional keys are present:

stack a stack trace of the fiber. This will be an empty list if the stack cannot be determined.
messages list of messages on the fiber's queue.
proc ::fiber::fibers {{level summary}} {

    # Returns a dictionary containing information about currently running fibers
    #  level - controls how much information is returned
    #
    # The returned dictionary is indexed by the fiber name with the
    # corresponding value itself being a dictionary with the following keys:
    #   message_count - count of messages pending for the fiber
    #   state - the current state of the fiber
    # If $level is 'detail', the following additional keys are present:
    #   stack - a stack trace of the fiber. This will be an empty
    #      list if the stack cannot be determined.
    #   messages - list of messages on the fiber's queue.

    variable fibers

    # TBD - add kill state and links

    set result {}
    foreach {name info} [array get fibers] {
        set rec [list state [dict get $info state] links [dict get $info links] message_count [llength [dict get $info messages]]]
        if {$level eq "detail"} {
            lappend rec messages [dict get $info messages]
            if {[dict get $info state] eq "WAIT"} {
                lappend rec stack [[dict get $info coroutine] stack]
            } else {
                lappend rec stack {}
            }
        }
        lappend result $name $rec
    }
    return $result
}

get [::fiber]

fiber, Top

Gets a message from the message queue for the current fiber.

get vmessage args

Parameters
vmessage name of a variable in the caller's context where the message is to be stored
argsAdditional options.
-preserve BOOLEAN If BOOLEAN is false (default), the message is removed from the message queue before it is returned. If true, the retrived message is stays in the queue.
-sender SENDER if specified, only messages sent from the fiber SENDER are examined and returned.
-types TYPELIST if specified, only messages whose type is is one of types in TYPELIST are examined and returned.
-wait MS if specified, the command returns with a value of timeout after MS milliseconds if no message is received in that time.
Return value

Returns success when a message is retrieved, else timeout.

Description

Gets a message from the message queue for the current fiber.

The command retrieves the first matching message from the message queue of the calling fiber. By default, all messages in the queue match. The -sender and -types options may be specified to limit matches to those from a specific sender and of a specific type.

Messages are normally removed from the queue when they are returned. The -preserve option can be used to indicate that the returned message should stay in the queue.

On a successful return, the message is stored in the variable named vmessage in the caller's context. The message is a list consisting of four elements, the sender, the target fiber name, the message type, and the message content. See the send command for more details.

The receive command offers an alternative way to retrieve messages.

proc ::fiber::get {vmessage args} {

    # Gets a message from the message queue for the current fiber.
    #    vmessage - name of a variable in the caller's context where
    #      the message is to be stored
    #    -sender SENDER - if specified, only messages sent from the fiber
    #      SENDER are examined and returned.
    #    -types TYPELIST - if specified, only messages whose type is
    #      is one of types in TYPELIST are examined and returned.
    #    -preserve BOOLEAN - If BOOLEAN is false (default), the message is
    #      removed from the message queue before it is returned. If true,
    #      the retrived message is stays in the queue.
    #    -wait MS - if specified, the command returns with a
    #      value of 'timeout' after
    #      MS milliseconds if no message is received in that time.
    #
    # The command retrieves the first matching message from the message
    # queue of the calling fiber. By default, all messages in the queue
    # match. The -sender and -types options may be specified to limit
    # matches to those from a specific sender and of a specific type.
    #
    # Messages are normally removed from the queue when they are returned.
    # The -preserve option can be used to indicate that the returned message
    # should stay in the queue.
    #
    # On a successful return, the message is stored in the variable named
    # $vmessage in the caller's context.
    # The message is a list consisting of four elements, the sender,
    # the target fiber name, the message type, and the message content.
    # See the send command for more details.
    #
    # The [receive] command offers an alternative way to retrieve messages.
    #
    # Returns 'success' when a message is retrieved, else 'timeout'.

    variable fibers
    variable ready

    set name [me]

    set opts(-preserve) 0
    array set opts $args

    if {[info exists opts(-wait)]} {
        incr opts(-wait) 0;     # Error if not integer
        if {$opts(-wait)} {
            # Need a timer. Note at most one active timer is outstanding.
            set timer_start [clock milliseconds]
            dict set fibers($name) timeout_timerid [after $opts(-wait) [namespace current]::_receive_timeout $name]
        }
    }

    dict set fibers($name) state WAIT

    # If a message is available, return it right away. Otherwise, we will
    # yield to allow other fibers to run. The nested structure is because
    # we want to yield even when opts(wait) is specified as 0.
    if {![_receive $name message opts]} {
        # No message or no matching messages. Indicate to dispatcher
        # not to call us unless another message arrives for us or timeout
        dict unset ready $name

        # Do NOT check timer here. We want to yield first
        _yield
        # Do NOT check timer here as the yield might have resulted in new
        # messages. Does not mean the new message will match so we loop.

        while {![_receive $name message opts]} {
            # No message or no matching messages. Indicate to dispatcher
            # not to call us unless another message arrives
            dict unset ready $name

            # If timer active and timed out, indicate as such
            if {[info exists opts(-wait)]} {
                # Note because clock resolution is granular, but we do not have
                # to worry about it because the after timer always guarantees
                # at least the specified delay.
                if {$opts(-wait) == 0 ||
                    ([clock milliseconds] - $timer_start) > $opts(-wait)} {
                    return timeout
                }
            }

            # Keep waiting
            _yield
        }
    }

    after cancel [dict get $fibers($name) timeout_timerid]
    dict set fibers($name) state RUN
    # Indicate to dispatcher not to call us until we are ready for next
    # message.
    dict unset ready $name
    upvar 1 $vmessage result
    set result $message
    return success
}

kill [::fiber]

fiber, Top

Terminates the specified fiber

kill receiver args

Parameters
receiver name of the fiber to be terminated
argsAdditional options.
-message MESSAGE a string passed to the fiber termination handler Defaults to "Kill signal received"
-reason REASONCODE the code for termination. KILL by default.
-sender SENDER identifies invoker. If not specified or empty users the fiber name if called within a fiber
Return value

Returns 1 if a kill was signalled to the fiber and 0 if the signal was not sent because the fiber did not exist or was terminating or already had a kill signal queued.

Description

Terminates the specified fiber

Termination of the fiber is not an immediate hard termination. It is only at the point where a fiber makes a call that can context switch that a Tcl exception is thrown. If uncaught by the fiber, this will result in the fiber being terminated. The thrown exception errorCode has FIBER, EXIT as the first two elements, a kill-specific code as the third element and a reason description as the fourth element.

proc ::fiber::kill {receiver args} {

    # Terminates the specified fiber
    #    receiver - name of the fiber to be terminated
    #    -sender SENDER - identifies invoker. If not specified or empty
    #       users the fiber name if called within a fiber
    #    -message MESSAGE - a string passed to the fiber termination handler
    #       Defaults to "Kill signal received"
    #    -reason REASONCODE - the code for termination. 'KILL' by default.
    # Termination of the fiber is not an immediate hard termination.
    # It is only at the point where a fiber makes a call that can context
    # switch that a Tcl exception is thrown. If uncaught by the fiber,
    # this will result in the fiber being terminated.
    # The thrown exception $errorCode has 'FIBER', 'EXIT'
    # as the first two elements, a kill-specific code as the third element
    # and a reason description as the fourth element.
    #
    # Returns '1' if a kill was signalled to the fiber and '0' if
    # the signal was not sent because the fiber did not exist or was
    # terminating or already had a kill signal queued.

    variable fibers
    variable wakeup_dispatcher
    variable coro_names

    if {![info exists fibers($receiver)] ||
        [dict exists $fibers($receiver) kill] ||
        [dict get $fibers($receiver) state] eq "EXITING"} {
        return 0
    }

    array set opts [list -sender {} -reason KILL -message "$receiver: Kill signal received"]
    array set opts $args

    # The -errorcode option is undocumented and used internally
    # for delivering process termination notifications.
    if {![info exists opts(-errorcode)]} {
        set opts(-errorcode) [list FIBER EXIT $opts(-reason) $opts(-message)]
    }

    if {$opts(-sender) eq ""} {
        set coro [info coroutine]
        if {[info exists coro_names($coro)]} {
            set opts(-sender) $coro_names($coro)
        }
    }

    # We send a message if either it is NOT a hard KILL or the target is
    # trapping exits.
    if {$opts(-reason) ne "KILL" && [dict get $fibers($receiver) trap_exits]} {
        _send $opts(-sender) $receiver <exit> [list $opts(-errorcode) $opts(-message)]
    } else {
        dict set fibers($receiver) kill [list sender $opts(-sender) message $opts(-message) errorcode $opts(-errorcode)]
    }
    wakeup $receiver
    return 1
}

link [::fiber]

fiber, Top

Links fiber target to the calling fiber so each is signalled when the other terminates

link target

Parameters
target name of the fiber to link to
Description

Links fiber target to the calling fiber so each is signalled when the other terminates

When two fibers are linked, abnormal termination of one results in the other being signalled.

By default, if a fiber ends normally, no notification is delivered to the linked fibers. If a fiber aborts, either through an error or an explicit kill, the fibers linked to it are delivered 'kill' signals. This kill signal results in an Tcl exception being generated whenever the target fiber makes a context switching call. The associated errorcode for the exception is a list with FIBER, EXIT, LINKDEAD as the first three elements followed by a string describing the reason for the signal.

Fibers that do not wish to terminate in response to this signal should not accomplish this by catching the above exception. They should instead use the trap_exits command to indicate that the signal should be delivered as a message instead. In this case, when a linked fiber dies the message type <linkdead> is delivered via the fiber's message queue. The sender field of the message is the id of the exiting fiber and the content field provides the reason for termination.

Note that if signals are not being trapped, when a fiber is terminated because a linked fiber is terminated, other fibers linked to the first fiber are in turn terminated. In other words, the abnormal termination results in cascading of the signal to all fibers that are directly or indirectly linked to the terminating fiber.

The link command establishes a two-way association. See the monitor command for a one-way alternative. If both commands are used, the last one invoked will take effect.

proc ::fiber::link {target} {

    # Links fiber $target to the calling fiber so each is signalled
    # when the other terminates
    #    target - name of the fiber to link to
    # When two fibers are linked, abnormal termination of one results
    # in the other being signalled.
    #
    # By default,
    # if a fiber ends normally, no notification is delivered to the
    # linked fibers. If a fiber aborts, either through an error or
    # an explicit [kill], the fibers linked to it are delivered 'kill'
    # signals. This kill signal results in an Tcl exception being
    # generated whenever the target fiber makes a context switching
    # call. The associated $errorcode for the exception is a list with
    # 'FIBER', 'EXIT', 'LINKDEAD' as the first three elements followed by
    # a string describing the reason for the signal.
    #
    # Fibers that do not wish to terminate in response to this signal
    # should not accomplish this by catching the above exception.
    # They should instead use the [trap_exits] command to indicate
    # that the signal should be delivered as a message
    # instead. In this case, when a linked fiber dies the message type
    # '<linkdead>' is delivered via the fiber's message queue.
    # The 'sender' field of the message is the id of the exiting
    # fiber and the 'content' field provides the reason for termination.
    #
    # Note that if signals are not being trapped, when a fiber is
    # terminated because a linked fiber is terminated, other fibers
    # linked to the first fiber are in turn terminated. In other words,
    # the abnormal termination results in cascading of the signal to
    # all fibers that are directly or indirectly linked to the terminating
    # fiber.
    #
    # The [link] command establishes a two-way association. See the
    # [monitor] command for a one-way alternative. If both commands
    # are used, the last one invoked will take effect.

    variable fibers
    set me [me]
    if {$me eq $target} {
        barf [list FIBER BADLINK "Fiber $me attempted to link to itself."]
    }
    if {![info exists fibers($target)]} {
        barf [list FIBER NOTAFIBER "Fiber $target does not exist"]
    }
    dict set fibers($target) links $me link
    dict set fibers($me) links $target link
    return
}

make_ref [::fiber]

fiber, Top

Genrates a string that can be used as a unique reference within the lifetime of a node.

make_ref prefix

Parameters
prefix(optional, default ) a prefix to use for the reference
Return value

Returns the reference

Description

Genrates a string that can be used as a unique reference within the lifetime of a node.

Generated references can be used to uniquely identify and match requests/responses within a node. References can also be used as handles.

The generated reference is unique only during the current process lifetime and within the process. It is NOT a UUID.

proc ::fiber::make_ref {{prefix {}}} {

    # Genrates a string that can be used as a unique reference within
    # the lifetime of a node.
    #  prefix - a prefix to use for the reference
    # Generated references can be used to uniquely identify and match
    # requests/responses within a node. References can also be used
    # as handles.
    #
    # The generated reference is unique only during the current
    # process lifetime and within the process. It is NOT a UUID.
    #
    # Returns the reference
    variable ref_ctr

    return ${prefix}#[incr ref_ctr]
}

me [::fiber]

fiber, Top

Returns name of calling fiber

me

Return value

Returns name of calling fiber

proc ::fiber::me {} {

    # Returns name of calling fiber
    variable coro_names
    set coro [info coroutine]
    if {![info exists coro_names($coro)]} {
        barf [list FIBER NOTAFIBER "[lindex [info level 1] 0] called from outside a fiber"]
    }
    return $coro_names($coro)
}

monitor [::fiber]

fiber, Top

Starts monitoring the fiber target so that the calling fiber is notified when it is not running.

monitor target

Parameters
target name of the fiber to monitor
Return value

Returns a reference that is included in the corresponding received messages.

Description

Starts monitoring the fiber target so that the calling fiber is notified when it is not running.

If the fiber target does not exist, or when it terminates, either normally or through an exception, the calling fiber is sent a message. The message type is <monitor>, the sender field of the message is the id of the exiting fiber and the content field is a list of two or three elements. The first element is the reference returned by this command. This allows matching of monitor calls with corresponding messages. The second provides the reason for termination of target. If the fiber terminated normally, the reason will be NORMAL. If it is not currently running, the reason will be NOTAFIBER. Other codes depend on the specific cause of termination. The third element, if present, is a string describing the cause of termination.

The link command provides an alternative mechanism for detecting process exits. However, unlike the link command, monitor establishes a one-way association and always delivers the notification as a message, never a kill signal, irrespective of the setting of trap_exits. Also, unlike link, monitor also notifies normal fiber exits. If both commands link and monitor are used, the last one invoked will take effect.

proc ::fiber::monitor {target} {

    # Starts monitoring the fiber $target so that the calling fiber is
    # notified when it is not running.
    #    target - name of the fiber to monitor
    # If the fiber $target does not exist, or when it
    # terminates, either normally or through an
    # exception, the calling fiber is sent a message. The message type is
    # '<monitor>', the 'sender' field of the message is the id of the exiting
    # fiber and the 'content' field is a list of two or three elements.
    # The first element is the reference returned by this command. This
    # allows matching of [monitor] calls with corresponding messages.
    # The second provides the reason for termination
    # of $target. If the fiber terminated normally, the reason will
    # be 'NORMAL'. If it is not currently running, the reason will
    # be 'NOTAFIBER'. Other codes depend on the specific cause of
    # termination. The third element, if present, is a string
    # describing the cause of termination.
    #
    # The [link] command provides an alternative mechanism for detecting
    # process exits. However, unlike the [link] command, [monitor]
    # establishes a one-way association and always delivers the notification
    # as a message, never a kill signal, irrespective of the setting of
    # [trap_exits]. Also, unlike [link], [monitor]
    # also notifies normal fiber exits. If both commands [link] and [monitor]
    # are used, the last one invoked will take effect.
    #
    # Returns a reference that is included in the corresponding received
    # messages.

    variable fibers

    set ref [make_ref monitor]
    set me [me]
    if {$me eq $target} {
        barf [list FIBER BADLINK "Fiber $me attempted to link to itself."]
    }
    if {![info exists fibers($target)]} {
        _send_monitor_message $target $me $ref [list FIBER EXIT NOTAFIBER "Fiber $target does not exist"] "Fiber $target does not exist"
        return $ref
    }

    dict set fibers($target) links $me $ref
    return $ref
}

receive [::fiber]

fiber, Top

Runs a script in response to reception of a message.

receive messagevar switchargs script args

Parameters
messagevar name of array variable in the caller's context that will receive the sender, receiver, type and content fields from the received message
switchargs expression based on which the appropriate branch of script is executed.
script body in the same format as for a switch command. See the description for details
argsAdditional options.
-preserve BOOLEAN If BOOLEAN is false (default), the message is removed from the message queue before it is returned. If true, the retrived message is stays in the queue
-sender SENDER if specified, only messages sent from the fiber SENDER are examined
-types TYPELIST if specified, only messages whose type is is one of types in TYPELIST are examined
-wait MS if specified, the command delivers a <timeout> system message to script after MS milliseconds if no message is received in that time.
Return value

Returns the result of evaluation of script

Description

Runs a script in response to reception of a message.

The command retrieves the first matching message from the message queue of the calling fiber. By default, all messages in the queue match. The -sender and -types options may be specified to limit matches to those from a specific sender and of a specific type. Use of these options will not prevent <timeout> messages from being delivered if the -wait option has been specified.

Messages are normally removed from the queue when they are returned. The -preserve option can be used to indicate that the returned message should stay in the queue.

When a message is successfully retrieved, its sender, receiver, type and content fields are stored in the corresponding elements of the messagevar array in the caller's context. The command then evaluates script as the body of a [switch] command in the caller's context. If switchargs is one of the variable names in messagevar, the value of the variable selects the branch of script to be executed. Otherwise, switchargs is interpreted as arguments to the Tcl [switch] command and a branch of script is selected accordingly.

An example of the first form is

  while {1} {
      fiber::receive msg type {
           <line> {... Handle an input line ...}
           <eof> {break}
      }
  }

An example of the second form is

  while {1} {
      fiber::receive msg {-glob -- $msg(type),$msg(content)} {
           <line>,GET* {... Handle a GET command ...}
           <line>,PUT* {... Handle a PUT command ...}
           <eof>,* {break}
      }
  }

Note script may receive some system messages as well, in particular <timeout> and <error>.

The get command offers an alternative way to retrieve messages.

proc ::fiber::receive {messagevar switchargs script args} {

    # Runs a script in response to reception of a message.
    #    messagevar - name of array variable in the caller's context that will
    #      receive the 'sender', 'receiver', 'type' and 'content' fields
    #      from the received message
    #    switchargs - expression based on which the appropriate
    #      branch of $script is executed.
    #    script - body in the same format as for a switch command. See
    #      the description for details
    #    -sender SENDER - if specified, only messages sent from the fiber
    #      SENDER are examined
    #    -types TYPELIST - if specified, only messages whose type is
    #      is one of types in TYPELIST are examined
    #    -preserve BOOLEAN - If BOOLEAN is false (default), the message is
    #      removed from the message queue before it is returned. If true,
    #      the retrived message is stays in the queue
    #    -wait MS - if specified, the command delivers a '<timeout>' system
    #      message to $script after MS milliseconds if no message is
    #      received in that time.
    #
    # The command retrieves the first matching message from the message
    # queue of the calling fiber. By default, all messages in the queue
    # match. The -sender and -types options may be specified to limit
    # matches to those from a specific sender and of a specific type. Use
    # of these options will not prevent '<timeout>' messages from being
    # delivered if the -wait option has been specified.
    #
    # Messages are normally removed from the queue when they are returned.
    # The -preserve option can be used to indicate that the returned message
    # should stay in the queue.
    #
    # When a message is successfully retrieved, its 'sender', 'receiver',
    # 'type' and 'content' fields are stored
    # in the corresponding elements of the $messagevar array in the
    # caller's context. The command then evaluates $script as the body
    # of a [switch] command in the caller's context. If $switchargs
    # is one of the variable names in $messagevar, the value of the
    # variable selects the branch of $script to be executed.
    # Otherwise, $switchargs is interpreted as arguments to the Tcl
    # [switch] command and a branch of $script is selected accordingly.
    #
    # An example of the first form is
    #   while {1} {
    #       fiber::receive msg type {
    #            <line> {... Handle an input line ...}
    #            <eof> {break}
    #       }
    #   }
    #
    # An example of the second form is
    #   while {1} {
    #       fiber::receive msg {-glob -- $msg(type),$msg(content)} {
    #            <line>,GET* {... Handle a GET command ...}
    #            <line>,PUT* {... Handle a PUT command ...}
    #            <eof>,* {break}
    #       }
    #   }
    #
    # Note $script may receive some system messages as well,
    # in particular '<timeout>' and '<error>'.
    #
    # The [get] command offers an alternative way to retrieve messages.
    #
    # Returns the result of evaluation of $script

    variable fibers
    variable ready

    set name [me]

    set opts(-preserve) 0
    array set opts $args

    if {[info exists opts(-wait)]} {
        incr opts(-wait) 0;     # Error if not integer
        if {$opts(-wait)} {
            # Need a timer. Note at most one active timer is outstanding.
            set timer_start [clock milliseconds]
            dict set fibers($name) timeout_timerid [after $opts(-wait) [namespace current]::_receive_timeout $name]
        }
    }

    dict set fibers($name) state WAIT

    # If a message is available, return it right away. Otherwise, we will
    # yield to allow other fibers to run. The nested structure is because
    # we want to yield even when opts(wait) is specified as 0.
    if {![_receive $name message opts]} {
        # No message or no matching messages. Indicate to dispatcher
        # not to call us unless another message arrives for us or timeout
        dict unset ready $name

        # Do NOT check timer here. We want to yield first
        _yield
        # Do NOT check timer here as the yield might have resulted in new
        # messages. Does not mean the new message will match so we loop.

        while {![_receive $name message opts]} {
            # No message or no matching messages. Indicate to dispatcher
            # not to call us unless another message arrives for us or timeout
            dict unset ready $name

            # If timer active and timed out, indicate as such
            if {[info exists opts(-wait)]} {
                # Note because clock resolution is granular, but we do not have
                # to worry about it because the after timer always guarantees
                # at least the specified delay.
                if {$opts(-wait) == 0 ||
                    ([clock milliseconds] - $timer_start) >= $opts(-wait)} {
                    set message [list $name $name <timeout> "Receive timed out"]
                    break
                }
            }

            # Keep waiting
            _yield
        }
    }
    after cancel [dict get $fibers($name) timeout_timerid]
    dict set fibers($name) state RUN
    # Indicate to dispatcher not to call us until we are ready for next
    # message.
    dict unset ready $name

    # The tailcall try usage are attempts to get byte compilation.
    # Not sure it actually helps

    # The joins are needed to remove newlines etc. from $scriptexpr
    # TBD - must be a better way
    upvar 1 $messagevar msg
    lassign $message msg(sender) msg(receiver) msg(type) msg(content)
    if {$switchargs in {type sender content receiver}} {
        tailcall try "switch {$msg($switchargs)} [list $script]"
    } else {
        tailcall try "switch [join $switchargs] [list $script]"
    }
}

run [::fiber]

fiber, Top

Run fibers in round robin fashion, dispatching messages

run runmode

Parameters
runmode Controls execution life time of the command
Return value

Returns the current number of fibers.

Description

Run fibers in round robin fashion, dispatching messages

The runs every fiber in turn, notifying them of pending messages if any. The fibers must be in WAIT state for notifications to be delivered, for example, blocked on a get or receive command.

To protect against reentrancy issues, the command reschedules itself to run from the Tcl event loop if called from within a fiber.

When runmode is master, fiber runtime errors are reported in the background using the [bgerror] mechanism. If runmode is slave, an exception is generated.

The command will continue running each fiber in turn until all fibers are blocked. The operation of the command in that state depends on the runmode argument. If runmode is master, the command will then block, yielding to the event loop until some other program component sends a message to one of the fibers. If runmode is slave, the command returns when all fibers are blocked. It is the caller's responsibility to invoke the command again at the appropriate time.

In all cases, the command returns if there are no fibers in the system.

proc ::fiber::run {runmode} {

    # Run fibers in round robin fashion, dispatching messages
    #   runmode - Controls execution life time of the command
    #
    # The runs every fiber in turn, notifying them of pending messages if any.
    # The fibers must be in WAIT state for notifications to be delivered,
    # for example, blocked on a [get] or [receive] command.
    #
    # Returns the current number of fibers.

    variable fibers
    variable ready
    variable wakeup_dispatcher
    variable coro_names

    if {$runmode ni {master slave}} {
        barf [list FIBER BADARG "Invalid run mode '$runmode'. Must be 'slave' or 'master'"]
    }

    if {[info exists wakeup_dispatcher]} {
        return; # Dispatcher is already running somewhere on stack
    }

    if {[info exists coro_names([info coroutine])]} {
        #ruff
        # To protect against reentrancy issues, the command reschedules itself
        # to run from the Tcl event loop if called from within a fiber.
        after 0 [namespace current]::run $runmode
    }

    set wakeup_dispatcher 0

    try {
        while {1} {
            while {[dict size $ready]} {
                foreach name [dict keys $ready] {
                    # NOTE: in the current implementation, the cleanup
                    # for the fiber catches errors. If there are linked
                    # processes, they are notified and the error
                    # is no propagated here. If there are no linked
                    # processes, the error is propagated so we need
                    # to handle it.
                    if {[catch {
                        if {[dict exists $fibers($name) kill]} {
                            [dict get $fibers($name) coroutine] kill
                        } else {
                            [dict get $fibers($name) coroutine]
                        }
                    } msg options]} {
                        #ruff
                        # When $runmode is 'master', fiber runtime errors
                        # are reported in the background using the [bgerror]
                        # mechanism. If $runmode is 'slave', an exception
                        # is generated.
                        if {$runmode eq "master"} {
                            catch {[interp bgerror {}] $msg $options}
                        } else {
                            return -options $options $msg
                        }
                    }
                }
            }

            #ruff
            # The command will continue running each fiber in turn until
            # all fibers are blocked. The operation of the command in
            # that state depends on the $runmode argument. If $runmode is
            # 'master', the command will then block, yielding to the event loop
            # until some other program component sends a message to one of the
            # fibers. If $runmode is 'slave', the command returns when
            # all fibers are blocked. It is the caller's responsibility to
            # invoke the command again at the appropriate time.
            #
            # In all cases, the command returns if
            # there are no fibers in the system.

            set nfibers [array size fibers]
            if {$nfibers == 0 || $runmode ne "master"} {
                return $nfibers
            }

            vwait [namespace current]::wakeup_dispatcher
        }
    } finally {
        unset wakeup_dispatcher
    }
}

send [::fiber]

fiber, Top

Sends a message to a fiber

send receiver content args

Parameters
receiver name of the fiber to which the message is to be sent
content the content of the message
argsAdditional options.
-sender SENDER specifies the sender of the message
-type MSGTYPE specifies the type of the message
Return value

Returns 1 if the message was queued successfully and 0 otherwise.

Description

Sends a message to a fiber

The command places a message on the end of the message queue for the specified fiber. It is not delivered synchronously but is rather is read by the fiber when it invokes the receive command.

A message is a list of four elements: the name of the sender, the name of the intended receiver, a message type and the content.

The message type defaults to application if the -type option is not specified. MSGTYPE may be any string denoting an application specific type except strings enclosed in <> which are reserved for internal use although this is not enforced.

If the -sender option is not specified, the sender is the name of the calling fiber or the empty string if not called from a fiber.

proc ::fiber::send {receiver content args} {

    # Sends a message to a fiber
    #    receiver - name of the fiber to which the message is to be sent
    #    content - the content of the message
    #    -sender SENDER - specifies the sender of the message
    #    -type MSGTYPE - specifies the type of the message
    #
    # The command places a message on the end of the message queue for
    # the specified fiber. It is not delivered synchronously but is rather
    # is read by the fiber when it invokes the receive command.
    #
    # A message is a list of four elements: the name of the sender, the name of
    # the intended receiver, a message type and the content.
    #
    # Returns 1 if the message was queued successfully and 0 otherwise.

    variable fibers
    variable wakeup_dispatcher
    variable coro_names

    if {![info exists fibers($receiver)] || [dict get $fibers($receiver) state] eq "EXITING"} {
        return 0;      # Return value indicates queueing, not delivery
    }

    #ruff
    # The message type defaults to 'application' if the -type option
    # is not specified.  MSGTYPE may be any string denoting an
    # application specific type except strings enclosed in '<>' which
    # are reserved for internal use although this is not enforced.
    #
    set type application
    if {[dict size $args]} {
        if {[dict exists $args -sender]} {
            set from [dict get $args -sender]
        }
        if {[dict exists $args -type]} {
            set type [dict get $args -type]
        }
        if {$type eq "<null>"} {
            barf [list FIBER BADARG "Message type '<null>' is not allowed."]
        }
    }

    #ruff
    # If the -sender option is not specified, the sender is the name of the
    # calling fiber or the empty string if not called from a fiber.
    if {![info exists from]} {
        if {[info exists coro_names([info coroutine])]} {
            set from $coro_names([info coroutine])
        } else {
            set from ""
        }
    }

    _send $from $receiver $type $content
    return 1
}

sleep [::fiber]

fiber, Top

Suspends the calling fiber for a specified time

sleep ms

Parameters
ms(optional, default 0) number of milliseconds to suspend
Description

Suspends the calling fiber for a specified time

This command is used to suspend the calling fiber for a specific amount of time while permitting other code to run. Even if ms is 0, the command will return only after giving other fibers a chance to execute.

proc ::fiber::sleep {{ms 0}} {

    # Suspends the calling fiber for a specified time
    #   ms - number of milliseconds to suspend
    # This command is used to suspend the calling fiber for a specific amount
    # of time while permitting other code to run. Even if $ms is '0', the
    # command will return only after giving other fibers a chance to execute.

    # Message type <null> should never be sent so we will timeout
    receive {} type {
        default {}
    } -wait $ms -from [me] -type <null>

    return
}

trap_exits [::fiber]

fiber, Top

Traps signalled exits and converts them to messages

trap_exits trap

Parameters
trap(optional, default 1) if true, kill signals are converted to messages. If false they are delivered as Tcl exceptions
Description

Traps signalled exits and converts them to messages

See the link command for more information.

proc ::fiber::trap_exits {{trap 1}} {

    # Traps signalled exits and converts them to messages
    #   trap - if true, kill signals are converted to messages. If false
    #      they are delivered as Tcl exceptions
    # See the [link] command for more information.
    variable fibers

    set me [me]
    dict set fibers($me) trap_exits [_boolean $trap]
    return
}

vars [::fiber]

fiber, Top

Define variables local to a fiber

vars args

Parameters
args list of variable names
Description

Define variables local to a fiber

The given variable names are mapped in the caller's context to fiber-specific variables of the same name.

proc ::fiber::vars {args} {

    # Define variables local to a fiber
    #  args - list of variable names
    #
    # The given variable names are mapped in the caller's context
    # to fiber-specific variables of the same name.

    # If we are being called from the level 1 context, no need
    # to do anything. The variables are in that context and in
    # fact doing an upvar would raise an error

    if {[info level] > 2} {
        set vars {}
        foreach n $args {lappend vars $n $n}
        tailcall upvar #1 {*}$vars
    }
}

wakeup [::fiber]

fiber, Top

Wakes up the specified fiber if it is currently suspended

wakeup name

Parameters
name
Description

Wakes up the specified fiber if it is currently suspended

It is an error if the fiber does not exist

proc ::fiber::wakeup {name} {

    # Wakes up the specified fiber if it is currently suspended
    #
    # It is an error if the fiber does not exist

    variable wakeup_dispatcher
    variable ready
    variable fibers

    # If the fiber is not in a WAIT state, do not mark it as ready
    # else the run dispatcher will call it when it might have actually
    # done a yield to somewhere else
    if {[dict get $fibers($name) state] ne "WAIT"} {
        return
    }

    dict set ready $name 1

    if {[info exists wakeup_dispatcher]} {
        set wakeup_dispatcher 1; # Bring dispatcher out of vwait
    } else {
        # If we are not in a coroutine, run it ourselves. Otherwise schedule it
        if {[info coroutine] eq ""} {
            fiber::run slave
        } else {
            after 0 fiber::run slave
        }
    }
    return
}
Document generated by Ruff!