Threads

The initializer function scheme_basic_env creates the main Scheme thread; all other threads are created through calls to scheme_thread.

Information about each internal MzScheme thread is kept in a Scheme_Thread structure. A pointer to the current thread's structure is available as scheme_current_thread. A Scheme_Thread structure includes the following fields:

The list of all threads is kept in a linked list; scheme_first_thread points to the first thread in the list. The last thread in the list is always the main thread.

8.1  Integration with Threads

MzScheme's threads can break external C code under two circumstances:

If either of these circumstances occurs, MzScheme will probably crash.

8.2  Allowing Thread Switches

C code that performs substantial or unbounded work should occasionally call SCHEME_USE_FUEL -- actually a macro -- which allows MzScheme to swap in another Scheme thread to run, and to check for breaks on the current thread. In particular, if breaks are enabled, then SCHEME_USE_FUEL may trigger an exception.

The macro consumes an integer argument. On most platforms, where thread scheduling is based on timer interrupts, the argument is ignored. On some platforms, however, the integer represents the amount of ``fuel'' that has been consumed since the last call to SCHEME_USE_FUEL. For example, the implementation of vector->list consumes a unit of fuel for each created cons cell:

Scheme_Object *scheme_vector_to_list(Scheme_Object *vec)
{
  int i;
  Scheme_Object *pair = scheme_null;

  i = SCHEME_VEC_SIZE(vec);

  for (; i--; ) {
    SCHEME_USE_FUEL(1);
    pair = scheme_make_pair(SCHEME_VEC_ELS(vec)[i], pair);
  }

  return pair;
}

The SCHEME_USE_FUEL macro expands to a C block, not an expression.

8.3  Blocking the Current Thread

Embedding or extension code sometimes needs to block, but blocking should allow other MzScheme threads to execute. To allow other threads to run, block using scheme_block_until. This procedure takes two functions: a polling function that tests whether the blocking operation can be completed, and a prepare-to-sleep function that sets bits in fd_sets when MzScheme decides to sleep (because all MzScheme threads are blocked). Under Windows, an ``fd_set'' can also accommodate OS-level semaphores or other handles via scheme_add_fd_handle.

Since the functions passed to scheme_block_until are called by the Scheme thread scheduler, they must never raise exceptions, call scheme_apply, or trigger the evaluation of Scheme code in any way. The scheme_block_until function itself may call the current exception handler, however, in reaction to a break (if breaks are enabled).

When a blocking operation is associated with an object, then the object might make sense as an argument to object-wait-multiple. To extend the set of objects accepted by object-wait-multiple, either register polling and sleeping functions with scheme_add_evt, or register a semaphore accessor with scheme_add_evt_through_sema.

8.4  Threads in Embedded MzScheme with Event Loops

When MzScheme is embedded in an application with an event-based model (i.e., the execution of Scheme code in the main thread is repeatedly triggered by external events until the application exits) special hooks must be set to ensure that non-main threads execute correctly. For example, during the execution in the main thread, a new thread may be created; the new thread may still be running when the main thread returns to the event loop, and it may be arbitrarily long before the main thread continues from the event loop. Under such circumstances, the embedding program must explicitly allow MzScheme to execute the non-main threads; this can be done by periodically calling the function scheme_check_threads.

Thread-checking only needs to be performed when non-main threads exist (or when there are active callback triggers). The embedding application can set the global function pointer scheme_notify_multithread to a function that takes an integer parameter and returns void. This function is be called with 1 when thread-checking becomes necessary, and then with 0 when thread checking is no longer necessary. An embedding program can use this information to prevent unnecessary scheme_check_threads polling.

The below code illustrates how MrEd formerly set up scheme_check_threads polling using the wxWindows wxTimer class. (Any regular event-loop-based callback is appropriate.) The scheme_notify_multithread pointer is set to MrEdInstallThreadTimer. (MrEd no longer work this way, however.)

class MrEdThreadTimer : public wxTimer
{
 public:
  void Notify(void); /* callback when timer expires */
};

static int threads_go;
static MrEdThreadTimer *theThreadTimer;
#define THREAD_WAIT_TIME 40

void MrEdThreadTimer::Notify()
{
  if (threads_go)
    Start(THREAD_WAIT_TIME, TRUE);

  scheme_check_threads();
}

static void MrEdInstallThreadTimer(int on)
{
  if (!theThreadTimer)
    theThreadTimer = new MrEdThreadTimer;

  if (on)
    theThreadTimer->Start(THREAD_WAIT_TIME, TRUE);
  else
    theThreadTimer->Stop();

  threads_go = on;
  if (on)
    do_this_time = 1;
}

An alternate architecture, which MrEd now uses, is to send the main thread into a loop, which blocks until an event is ready to handle. MzScheme automatically takes care of running all threads, and it does so efficiently because the main thread blocks on a file descriptor, as explained in section 8.3.

8.4.1  Callbacks for Blocked Threads

Scheme threads are sometimes blocked on file descriptors, such as an input file or the X event socket. Blocked non-main threads do not block the main thread, and therefore do not affect the event loop, so scheme_check_threads is sufficient to implement this case correctly. However, it is wasteful to poll these descriptors with scheme_check_threads when nothing else is happening in the application and when a lower-level poll on the file descriptors can be installed. If the global function pointer scheme_wakeup_on_input is set, then this case is handled more efficiently by turning off thread checking and issuing a ``wakeup'' request on the blocking file descriptors through scheme_wakeup_on_input.

A scheme_wakeup_on_input procedure takes a pointer to an array of three fd_sets (sortof4) and returns void. The scheme_wakeup_on_input does not sleep; it just sets up callbacks on the specified file descriptors. When input is ready on any of those file descriptors, the callbacks are removed and scheme_wake_up is called.

For example, the X Windows version of MrEd formerly set scheme_wakeup_on_input to this MrEdNeedWakeup:

static XtInputId *scheme_cb_ids = NULL;
static int num_cbs;

static void MrEdNeedWakeup(void *fds)
{
  int limit, count, i, p;
  fd_set *rd, *wr, *ex;

  rd = (fd_set *)fds;
  wr = ((fd_set *)fds) + 1;
  ex = ((fd_set *)fds) + 2;

  limit = getdtablesize();

  /* See if we need to do any work, really: */
  count = 0;
  for (i = 0; i < limit; i++) {
    if (MZ_FD_ISSET(i, rd))
      count++;
    if (MZ_FD_ISSET(i, wr))
      count++;
    if (MZ_FD_ISSET(i, ex))
      count++;
  }

  if (!count)
    return;

  /* Remove old callbacks: */
  if (scheme_cb_ids)
    for (i = 0; i < num_cbs; i++)
      notify_set_input_func((Notify_client)NULL, (Notify_func)NULL, 
                            scheme_cb_ids[i]);

  num_cbs = count;
  scheme_cb_ids = new int[num_cbs];

  /* Install callbacks */
  p = 0;
  for (i = 0; i < limit; i++) {
    if (MZ_FD_ISSET(i, rd))
      scheme_cb_ids[p++] = XtAppAddInput(wxAPP_CONTEXT, i, 
                                         (XtPointer *)XtInputReadMask, 
                                         (XtInputCallbackProc)MrEdWakeUp, NULL);
    if (MZ_FD_ISSET(i, wr))
      scheme_cb_ids[p++] = XtAppAddInput(wxAPP_CONTEXT, i, 
                                         (XtPointer *)XtInputWriteMask, 
                                         (XtInputCallbackProc)MrEdWakeUp, NULL);
    if (MZ_FD_ISSET(i, ex))
      scheme_cb_ids[p++] = XtAppAddInput(wxAPP_CONTEXT, i, 
                                         (XtPointer *)XtInputExceptMask, 
                                         (XtInputCallbackProc)MrEdWakeUp, 
                                         NULL);
  }
}

/* callback function when input/exception is detected: */
Bool MrEdWakeUp(XtPointer, int *, XtInputId *)
{
  int i;

  if (scheme_cb_ids) {
    /* Remove all callbacks: */
    for (i = 0; i < num_cbs; i++)
     XtRemoveInput(scheme_cb_ids[i]);

    scheme_cb_ids = NULL;
    
    /* ``wake up'' */
    scheme_wake_up();
  }

  return FALSE;
}

8.5  Sleeping by Embedded MzScheme

When all MzScheme threads are blocked, MzScheme must ``sleep'' for a certain number of seconds or until external input appears on some file descriptor. Generally, sleeping should block the main event loop of the entire application. However, the way in which sleeping is performed may depend on the embedding application. The global function pointer scheme_sleep can be set by an embedding application to implement a blocking sleep, although MzScheme implements this function for you.

A scheme_sleep function takes two arguments: a float and a void *. The latter is really points to an array of three ``fd_set'' records (one for read, one for write, and one for exceptions); these records are described further below. If the float argument is non-zero, then the scheme_sleep function blocks for the specified number of seconds, at most. The scheme_sleep function should block until there is input one of the file descriptors specified in the ``fd_set,'' indefinitely if the float argument is zero.

The second argument to scheme_sleep is conceptually an array of three fd_set records, but always use scheme_get_fdset to get anything other than the zeroth element of this array, and manipulate each ``fd_set'' with MZ_FD_XXX instead of FD_XXX.

The following function mzsleep is an appropriate scheme_sleep function for most any Unix or Windows application. (This is approximately the built-in sleep used by MzScheme.)

void mzsleep(float v, void *fds)
{
  if (v) {
    sleep(v);
  } else {
    int limit;
    fd_set *rd, *wr, *ex;
    
# ifdef WIN32
    limit = 0;
# else
    limit = getdtablesize();
# endif

    rd = (fd_set *)fds;
    wr = (fd_set *)scheme_get_fdset(fds, 1);
    ex = (fd_set *)scheme_get_fdset(fds, 2);

    select(limit, rd, wr, ex, NULL);
  }
}

8.6  Library Functions

¤ Scheme_Object *scheme_thread(Scheme_Object *thunk)

Creates a new thread, just like thread.

¤ Scheme_Object *scheme_thread_w_details(Scheme_Object *thunk, Scheme_Config *config,
    Scheme_Thread_Cell_Table *cells,
    Scheme_Custodian *cust
    int suspend_to_kill)

Like scheme_thread, except that the created thread belongs to cust instead of the current custodian, it uses the given config for its initial configuration, it uses cells for its thread-cell table, and if suspend_to_kill is non-zero, then the thread is merely suspended when it would otherwise be killed (through either kill-thread or custodian-shutdown-all).

The config argument is typically obtained through scheme_current_config or scheme_extend_config. A config is immutable, so different threads can safely use the same value. The cells argument should be obtained from scheme_inherit_cells; it is mutable, and a particular cell table should be used by only one thread.

¤ Scheme_Object *scheme_make_sema(long v)

Creates a new semaphore.

¤ void scheme_post_sema(Scheme_Object *sema)

Posts to sema.

¤ int scheme_wait_sema(Scheme_Object *sema, int try)

Waits on sema. If try is not 0, the wait can fail and 0 is returned for failure, otherwise 1 is returned.

¤ void scheme_thread_block(float sleep_time)

Allows the current thread to be swapped out in favor of other threads. If sleep_time positive, then the current thread will sleep for at least sleep_time seconds.

After calling this function, a program should almost always call scheme_making_progress next. The exception is when scheme_thread_block is called in a polling loop that performs no work that affects the progress of other threads. In that case, scheme_making_progress should be called immediately after exiting the loop.

See also scheme_block_until, and see also the SCHEME_USE_FUEL macro in section 8.2.

¤ void scheme_thread_block_enable_break(float sleep_time, int break_on)

Like scheme_thread_block, but breaks are enabled while blocking if break_on is true.

¤ void scheme_swap_thread(Scheme_Thread *thread)

Swaps out the current thread in favor of thread.

¤ void scheme_break_thread(Scheme_Thread *thread)

Sends a break signal to the given thread.

¤ int scheme_break_waiting(Scheme_Thread *thread)

Returns 1 if a break from break-thread or scheme_break_thread has occurred in the specified thread but has not yet been handled.

¤ int scheme_block_until(Scheme_Ready_Fun f, Scheme_Needs_Wakeup_Fun fdf,
   Scheme_Object *data, float sleep)

The Scheme_Ready_Fun and Scheme_Needs_Wakeup_Fun types are defined as follows:

   typedef int (*Scheme_Ready_Fun)(Scheme_Object *data);
   typedef void (*Scheme_Needs_Wakeup_Fun)(Scheme_Object *data, void *fds);

Blocks the current thread until f with data returns a true value. The f function is called periodically -- at least once per potential swap-in of the blocked thread -- and it may be called multiple times even after it returns a true value. If f with data ever returns a true value, it must continue to return a true value until scheme_block_until returns. The argument to f is the same data as provided to scheme_block_until, and data is ignored otherwise. (The data argument is not actually required to be a Scheme_Object* value, because it is only used by f and fdf.)

If MzScheme decides to sleep, then the fdf function is called to sets bits in fds, conceptually an array of three fd_sets: one or reading, one for writing, and one for exceptions. Use scheme_get_fdset to get elements of this array, and manipulate an ``fd_set'' with MZ_FD_XXX instead of FD_XXX. Under Windows, an ``fd_set'' can also accommodate OS-level semaphores or other handles via scheme_add_fd_handle.

The fdf argument can be NULL, which implies that the thread becomes unblocked (i.e., ready changes its result to true) only through Scheme actions, and never through external processes (e.g., through a socket or OS-level semaphore).

If sleep is a positive number, then scheme_block_until polls f at least every sleep seconds, but scheme_block_until does not return until f returns a true value. The call to scheme_block_until can return before sleep seconds if f returns a true value.

The return value from scheme_block_until is the return value of its most recent call to f, which enables f to return some information to the scheme_block_until caller.

See section 8.3 for information about restrictions on the f and fdf functions.

¤ int scheme_block_until_enable_break(Scheme_Ready_Fun f, Scheme_Needs_Wakeup_Fun fdf,
   Scheme_Object *data, float sleep,
   int break_on)

Like scheme_block_until, but breaks are enabled while blocking if break_on is true.

¤ int scheme_block_until_unless(Scheme_Ready_Fun f, Scheme_Needs_Wakeup_Fun fdf,
   Scheme_Object *data, float sleep,
   Scheme_Object *unless_evt, int break_on)

Like scheme_block_until_enable_break, but the function returns if unless_evt becomes ready, where unless_evt is a port progress event implemented by scheme_progress_evt_via_get. See scheme_make_input_port for more information.

¤ void scheme_check_threads()

This function is periodically called by the embedding program to give background processes time to execute. See section 8.4 for more information.

¤ void scheme_wake_up()

This function is called by the embedding program when there is input on an external file descriptor. See section 8.5 for more information.

¤ void *scheme_get_fdset(void *fds, int pos)

Extracts an ``fd_set'' from an array passed to scheme_sleep, a callback for scheme_block_until, or an input port callback for scheme_make_input_port.

¤ void scheme_add_fd_handle(void *h, void *fds, int repost)

Adds an OS-level semaphore (Windows) or other waitable handle (Windows) to the ``fd_set'' fds. When MzScheme performs a ``select'' to sleep on fds, it also waits on the given semaphore or handle. This feature makes it possible for MzScheme to sleep until it is awakened by an external process.

MzScheme does not attempt to deallocate the given semaphore or handle, and the ``select'' call using fds may be unblocked due to some other file descriptor or handle in fds. If repost is a true value, then h must be an OS-level semaphore, and if the ``select'' unblocks due to a post on h, then h is reposted; this allows clients to treat fds-installed semaphores uniformly, whether or not a post on the semaphore was consumed by ``select''.

The scheme_add_fd_handle function is useful for implementing the second procedure passed to scheme_wait_until, or for implementing a custom input port.

Under Unix and Mac OS X, this function has no effect.

¤ void scheme_add_fd_eventmask(void *fds, int mask)

Adds an OS-level event type (Windows) to the set of types in the ``fd_set'' fds. When MzScheme performs a ``select'' to sleep on fds, it also waits on events of them specified type. This feature makes it possible for MzScheme to sleep until it is awakened by an external process.

The event mask is only used when some handle is installed with scheme_add_fd_handle. This awkward restriction may force you to create a dummy semaphore that is never posted.

Under Unix, and Mac OS X, this function has no effect.

¤ void scheme_add_evt(Scheme_Type type, Scheme_Ready_Fun ready,
    Scheme_Needs_Wakeup_Fun wakeup, Scheme_Wait_Filter_Fun filter
    int can_redirect)

The argument types are defined as follows:

   typedef int (*Scheme_Ready_Fun)(Scheme_Object *data);
   typedef void (*Scheme_Needs_Wakeup_Fun)(Scheme_Object *data, void *fds);
   typedef int (*Scheme_Wait_Filter_Fun)(Scheme_Object *data);

Extends the set of waitable objects for object-wait-multiple to those with the type tag type. If filter is non-NULL, it constrains the new waitable set to those objects for which filter returns a non-zero value.

The ready and wakeup functions are used in the same way was the arguments to scheme_block_until.

The can_redirect argument should be 0.

¤ void scheme_add_evt_through_sema(Scheme_Type type, Scheme_Wait_Sema_Fun getsema,
    Scheme_Wait_Filter_Fun filter)

Like scheme_add_evt, but for objects where waiting is based on a semaphore. Instead of ready and wakeup functions, the getsema function extracts a semaphore for a given object:

   typedef Scheme_Object *(*Scheme_Wait_Sema_Fun)(Scheme_Object *data, int *repost);

If a successful wait should leave the semaphore waited, then getsema should set *repost to 0. Otherwise, the given semaphore will be re-posted after a successful wait. A getsema function should almost always set *repost to 1.

¤ void scheme_making_progress()

Notifies the scheduler that the current thread is not simply calling scheme_thread_block in a loop, but that it is actually making progress.

¤ int scheme_tls_allocate()

Allocates a thread local storage index to be used with scheme_tls_set and scheme_tls_get.

¤ void scheme_tls_set(int index, void *v)

Stores a thread-specific value using an index allocated with scheme_tls_allocate.

¤ void *scheme_tls_get(int index)

Retrieves a thread-specific value installed with scheme_tls_set. If no thread-specific value is available for the given index, NULL is returned.

¤ Scheme_Object *scheme_call_enable_break(Scheme_Prim *prim, int argc, Scheme_Object **argv)

Calls prim with the given argc and argv with breaks enabled. The prim function can block, in which case it might be interrupted by a break. The prim function should not block, yield, or check for breaks after it succeeds, where ``succeeds'' depends on the operation. For example, tcp-accept/enable-break is implemented by wrapping this function around the implementation of tcp-accept; the tcp-accept implementation does not block or yield after it accepts a connection.

¤ Scheme_Object *scheme_make_thread_cell(Scheme_Object *def_val, int preserved)

Creates a thread cell, like make-thread-cell.

¤ Scheme_Object *scheme_thread_cell_get(Scheme_Object *cell,
    Scheme_Thread_Cell_Table *cells)

Accesses a thread-specific value from a thread cell, like thread-cell-ref. The second argument is typically scheme_current_thread->cell_values to get a value for the current thread.

¤ void scheme_thread_cell_set(Scheme_Object *cell,
    Scheme_Thread_Cell_Table *cells, Scheme_Object *v)

Sets a thread-specific value for a thread cell, like thread-cell-set!. The second argument is typically scheme_current_thread->cell_values to set a value for the current thread.

¤ void scheme_start_atomic()

Prevents MzScheme thread swaps until scheme_end_atomic or scheme_end_atomic_no_swap is called. Start-atomic and end-atomic pairs can be nested.

¤ void scheme_end_atomic()

Ends an atomic region with respect to MzScheme threads. The current thread may be swapped out immediately (i.e., the call to scheme_end_atomic is assumed to be a safe point for thread swaps).

¤ void scheme_end_atomic_no_swap()

Ends an atomic region with respect to MzScheme threads, and also prevents an immediate thread swap. (In other words, no MzScheme thread swaps will occur until a future safe point.)


4 To ensure maximum portability, use MZ_FD_XXX instead of FD_XXX.