Tamer language & library manual

Tamer home - Compiler manual

tamer

C++ extensions for event-driven programming

SYNOPSIS

#include <tamer/tamer.hh> 
using namespace tamer; 


class event<> { public: event(); operator bool() const; bool empty() const; void trigger(); void operator()(); // synonym for trigger() void at_trigger(const event<>& trigger_evt); } ... class event<T0, T1, T2, T3> { public: event(); operator bool() const; bool empty() const; void trigger(const T0& v0, const T1& v1, const T2& v2, const T3& v3); void at_trigger(const event<>& trigger_evt); }

class rendezvous<I> { public: rendezvous(); event<> make_event(const I&
eid); event<T0> make_event(const I& eid, T0& s0); event<T0, T1> make_event(const I& eid, T0& s0, T1& s1); event<T0, T1, T2> make_event(const I& eid, T0& s0, T1& s1, T2& s2); event<T0, T1, T2, T3> make_event(const I& eid, T0& s0, T1& s1, T2& s2, T3& s3); bool has_events() const; bool has_ready() const; bool has_waiting() const; bool join(I& eid); }

class rendezvous<> { public: rendezvous(); event<> make_event(); event<T0> make_event(T0&
s0); event<T0, T1> make_event(T0& s0, T1& s1); event<T0, T1, T2> make_event(T0& s0, T1& s1, T2& s2); event<T0, T1, T2, T3> make_event(T0& s0, T1& s1, T2& s2, T3& s3); bool has_events() const; bool has_ready() const; bool has_waiting() const; bool join(); }

tamed void user_function(...) { tvars { ... }; }

twait { ... } twait volatile { ... }

twait(rendezvous<>&
rendezvous); twait(rendezvous<I>& rendezvous, I& eid);

DESCRIPTION

Tamer is a set of C++ language extensions and libraries that simplify the practice of event-driven programming.

This manual page gives an overview of the Tamer abstractions, and describes the user-accessible Tamer methods and functions. Most Tamer programs will also use the tamer(1) preprocessor, which converts programs using twait into conventional C++.

OVERVIEW

Tamer introduces four related abstractions for handling concurrency: events, wait points, rendezvous, and safe local variables.

Each event object represents a future occurrence, such as the completion of a network read. When the occurrence actually happens—for instance, a packet arrives—the event is triggered via its trigger method. Each active event is associated with exactly one rendezvous object, which represents a set of related events. A function can block until some event on a rendezvous occurs using a twait special form. For example, this function uses Tamer to print Done! after 10 seconds have elapsed. During that 10 seconds, the function blocks, allowing other application code to continue processing.

  tamed void f() { 
      twait { tamer::at_delay_sec(10, make_event()); } 
      printf("Done!\n"); 
  } 

Events

To create an event, call the make_event function of a rendezvous object.



rendezvous<> r; event<> e = r.make_event();

The event’s trigger method is called to indicate that the occurrence has happened. Triggering an event can unblock other functions that were waiting for the occurrence.


e.trigger();

Events are associated with between zero and four results, which pass information from trigger points back to blocked functions. For example, a result could indicate whether a network connection attempt succeeded or timed out. The make_event function takes references to results; the trigger method takes values to which the results are set. The types of an event’s results are given by its template arguments. For example:


rendezvous<> r; int i = 0; event<int> e = r.make_event(i); e.trigger(100); assert(i == 100); // assertion will succeed

Here, we see that the trigger value 100 is passed through to the result i. The type of i is echoed in e’s type event<int>.

event objects may be freely assigned, passed as arguments, and copied. They are automatically reference counted. Each event may be triggered at most once. An event that hasn’t triggered yet is called active. Triggering an active event makes it empty; triggering an empty event has no effect.

Tamer automatically triggers an active event when its last reference goes out of scope. This case is considered a programming error, however, and an dropping last reference message is printed to standard error. Explicitly calling rendezvous::clear on the event’s rendezvous will avoid the message, as will using a volatile rendezvous.

Wait Points and Rendezvous

The wait point language extension, written twait, blocks the calling function until one or more events are triggered. A blocked function returns to its caller, but does not actually complete. The function’s safe local variables and blocking point are preserved in a closure. Later, the function can unblock and resume execution. By that time, of course, the function’s original caller may have returned. Any function containing a wait point is marked with the tamed keyword, which informs the caller that the function can block.

The first, and more common, form of wait point is written twait { statements; }. This executes statements, then blocks until all events created in statements have triggered. (Within statements, make_event is redefined to a macro that automatically supplies a rendezvous argument.) For example, code like twait { tamer::at_delay_sec(10, make_event()); } should be read as execute tamer::at_delay_sec(10, make_event()), then block until the created event has triggered—or, since tamer::at_delay_sec triggers its event argument after the given number of seconds has passed, simply as block for 10 seconds.

The second form of wait point explicitly names a rendezvous object. A wait point twait(r) unblocks when any of rendezvous<> r’s events occurs. Unblocking consumes the event and restarts the blocked function. The twait() form can also return information about which event occurred. A rendezvous of type rendezvous<I> associates an event ID of type I with each event. The make_event function specifies the event ID as well as results. A twait(r, eid) statement sets the variable eid to the ID of the unblocking event. The type of eid must match the type of the rendezvous.

rendezvous objects have private copy constructors and assignment operators, preventing them from being copied.

A tamed function’s caller resumes when the called function either returns or blocks. A tamed function will often accept an event argument, which it triggers when it completes its processing. This lets the caller block until the function completes. Here is a tamed function that blocks, then returns an integer:



tamed void blockf(event<int> done) { ... block ... done.trigger(200); }

A caller will most likely use twait to wait for blockf to return, and so become tamed itself. Waiting for events thus trickles up the call stack until a caller doesn’t care whether its callee returns or blocks.

When an event e is triggered, Tamer enqueues a trigger notification for e’s event ID on e’s rendezvous r. This step also unblocks any function blocked on twait(r). Conversely, twait(r) checks for any queued trigger notifications r. If one exists, it is dequeued and returned. Otherwise, the function blocks at that wait point; it will unblock and recheck the rendezvous once someone triggers a corresponding event. The top-level event loop cycles through unblocked functions, calling them in some order.

Multiple functions cannot simultaneously block on the same rendezvous.

Safe Local Variables

Finally, safe local variables are variables whose values are preserved across wait points. The programmer marks local variables as safe by enclosing them in a tvars{} block, which preserves their values in a heap-allocated closure. Function parameters are always safe. Unsafe local variables have indeterminate values after a wait point. The C++ compiler will often give you an uninitialized-variable warning when a variable needs to be made safe.

EVENT CLASS

The event template class represents future occurrences. The template takes zero to four type arguments, which represent the types of the event’s results. In the following, are the template arguments of the event type. These type arguments must be copy-constructible and assignable.

event<Ts>::event() 

Creates an empty event. Trigger attempts on the event are ignored; e.empty() returns true.



event<Ts>::event(const event<Ts>& e) 
event<Ts>& event<Ts>::operator=(const event<Ts>& e) 

Events may be safely copied and assigned. After an assignment e1 = e2, the event objects e1 and e2 refer to the same underlying occurrence. Triggering either causes both to become empty.



event<Ts>::operator bool() const 

Returns true if the event is active. Empty events return false.



bool event<Ts>::empty() const 

Returns true if the event is empty, meaning it was created empty or has already been triggered. e.empty() is equivalent to !(bool)e.



void event<T0, T1, T2, T3>::trigger(const T0& v0, const T1& v1, 
                                    const T2& v2, const T3& v3) 
... void event<>::trigger() 

Triggers the event. If the event is empty, this does nothing; otherwise, it sets the event’s results (defined at creation time) to the trigger values v0...v3 and wakes any blocked closure. Events become empty after they are triggered.



void event<Ts>::at_trigger(const event<>& trigger_evt) 

Registers trigger_evt for cancel notification. If this event is already empty, trigger_evt is triggered immediately. Otherwise, trigger_evt is triggered when this event is triggered.



event<> event<Ts>::unblocker() const 

Returns a version of this event that has no results. The returned event refers to the same occurrence as this event, so triggering either event makes both events appear empty. However, unblocker().trigger() will leave this event’s results unchanged.

RENDEZVOUS CLASS

The rendezvous template class groups related events. The template takes an optional type argument, which is the type of the rendezvous’s event IDs. In the following, I is the template argument of the rendezvous type. If it is given, I objects must be copy-constructible and assignable.

rendezvous<I>::rendezvous() 

Creates a new rendezvous with no outstanding events.



bool rendezvous<I>::has_events() const 

Tests if there are any outstanding events. This includes events that have not yet triggered, and events that have triggered, but the trigger notification has not been collected yet.



bool rendezvous<I>::has_ready() const 

Tests if there are any ready events. An event is ready if it has been triggered, but the trigger notification has not been collected yet. The rendezvous<I>::join method will return true only if has_ready() is true.



bool rendezvous<I>::has_waiting() const

Tests if there are any waiting events. An event is waiting if it has not yet triggered.



bool rendezvous<I>::join(I& eid) 
bool rendezvous<>::join() 

Collects a trigger notification, if any events have triggered but have not yet been collected. If a trigger notification is available, sets the event ID argument eid, if any, to the collected event’s ID and returns true. Otherwise, returns false. The twait special forms are built around calls to rendezvous<I>::join.



void rendezvous<I>::clear() 

Removes all pending events from this rendezvous. Any active events on this rendezvous are effectively triggered, calling their at_trigger() notifiers and making the events themselves empty. After clear(), the rendezvous’s has_events() method returns false.

EVENT MODIFIERS

These functions manipulate events generically, for example by returning one event that triggers two others.

event<> operator+(const event<>& e1, const event<>& e2) 

Returns an event that combines e1 and e2. Triggering the returned event will trigger both e1 and e2. The returned event is empty if and only if both e1 and e2 are empty. tamer::all(e1, e2) is a synonym for operator+.



event<> bind(const event<T0>& e, const T0& v0) 

Returns an event that, when triggered, will call e.trigger(v0).



event<T0> rebind(const event<>& e) 

Returns an event that, when triggered, will call e.trigger(). The returned event’s trigger value is ignored.

DRIVER

The driver class handles Tamer’s fundamental events: timers, signals, and file descriptors. Most programs will use the single driver::main object, which is accessed through top-level functions as follows.

void at_fd_read(int fd, event<int> e) 
void at_fd_read(int fd, event<> e) 

Triggers e when fd becomes readable, or when fd is closed or encounters an error, whichever comes first. fd must be a valid file descriptor less than FD_SETSIZE. In the version taking event<int>, the trigger value is 0 when fd becomes readable, and a negative error code otherwise.



void at_fd_write(int fd, event<int> e) 
void at_fd_write(int fd, event<> e) 

Triggers event e when fd becomes writable. fd must be a valid file descriptor less than FD_SETSIZE. The trigger value is as for at_fd_read().



void at_time(const timeval& expiry, event<> e) 

Triggers event e on, or soon after, time expiry.



void at_delay(const timeval& delay, event<> e) 

Triggers event e after at least delay time has passed. All delays are measured relative to the timestamp now().



void at_delay(double delay, event<> e) 

Triggers event e after at least delay seconds have passed.



void at_delay_sec(int delay, event<> e) 

Triggers event e after at least delay seconds have passed.



void at_delay_msec(int delay, event<> e) 

Triggers event e after at least delay milliseconds have passed.



void at_signal(int signal, event<> e) 

Triggers event e if the signal occurs. The event is not triggered directly inside the signal handler. Rather, the signal handler marks the signal’s occurrence, then blocks the signal from further delivery. The signal remains blocked at least until e has been triggered and any corresponding closure has run (and possibly registered another event to catch the signal). Thus, programmers can safely catch signals without race conditions.



void at_asap(event<> e) 

Triggers event e on the next execution of Tamer’s main loop.



const timeval& now() 

Returns the current cached timestamp.



void once() 

Runs through the driver’s event loop once. First, the driver removes any empty timer and file descriptor events. Then, the driver calls select and possibly blocks, waiting for the next event. Then, the driver triggers and runs the appropriate signal events, file descriptor events, timer events, and ASAP events. Each path through the event loop resets now() to the correct current value.



void loop() 

Equivalent to while (true) once();.



void break_loop() 

Causes any active call to loop() to return.

CANCEL ADAPTERS

These functions integrate timeouts, signals, and other forms of cancellation into existing events. For example:

  int i;  rendezvous<> r;
  event<int> e = add_timeout(delay, r.make_event(i), -ETIMEDOUT);
The event on r is triggered on the first of the following events.

  • e is triggered. i is set to e’s trigger value.
  • delay seconds elapse. i is set to -ETIMEDOUT.

Cancel adapters are available for timeouts and signals.

event<T> add_timeout(const timeval& delay, event<T> e, const V& v) 
event<T> add_timeout_sec(int delay, event<T> e, const V& v) 
event<T> add_timeout_msec(int delay, event<T> e, const V& v) 

Adds a timeout to e. If the delay expires before e is triggered normally, then e is triggered with value v. Returns e.



event<T> add_signal(int signal, event<T> e, const V& v) 
event<T> add_signal(ITER first, ITER last, event<T> e, const V& v) 

Adds signal detection to e. If the signal (or one of the signals in the iterator range [first, last)) happens before e triggers normally, then e is triggered with value v. Returns e.

There is also a set of cancel adapters that don’t set e’s trigger value. For example:

  int i(-1);  rendezvous<> r;
  event<int> e = with_timeout(delay, r.make_event(i));
The event on r is triggered on the first of the following occurrences:

  • e is triggered. i is set to e’s trigger value.
  • delay seconds elapse. i retains its initial value.



event<Ts> with_timeout(const timeval& delay, event<Ts> e) 
event<Ts> with_timeout_sec(int delay, event<Ts> e) 
event<Ts> with_timeout_msec(int delay, event<Ts> e) 
event<Ts> with_signal(int signal, event<Ts> e) 
event<Ts> with_signal(ITER first, ITER last, event<Ts> e) 

Return cancel-adapted versions of e. These functions are analogous to the add_ versions above, but do not set any trigger values to indicate whether the event triggered successfully.



event<Ts> with_timeout(const timeval& delay, event<Ts> e, int& result) 
event<Ts> with_timeout_sec(int delay, event<Ts> e, int& result) 
event<Ts> with_timeout_msec(int delay, event<Ts> e, int& result) 
event<Ts> with_signal(int signal, event<Ts> e, int& result) 
event<Ts> with_signal(ITER first, ITER last, event<Ts> e, 
                      int& result) 

Return cancel-adapted versions of e. When e triggers, the result variable is set to one of the following constants to indicate why:
0
if e triggered successfully.
-ETIMEDOUT
if e timed out.
-EINTR
if e was interrupted by a signal.

The constants tamer::outcome::{success, timeout, signal} may be used instead of the error values.

FILE I/O

Tamer’s support for file I/O is available via #include <tamer/fd.hh>. Variants of the main I/O system calls are provided, most of them nonblocking. See tamer_fd(3).

BUGS

The existing fd wrappers are only truly nonblocking for pipe, socket, and network I/O. The functions will block on disk I/O.

The Tamer interface differs in several ways from the interface described in Events Can Make Sense by Krohn et al. First, all Tamer classes and functions are declared in the tamer namespace. using namespace tamer; will bring them into the global namespace. Second, Tamer events are created with make_event (rather than mkevent), which more closely follows the C++ standard library’s style. Third, Tamer primitive events are registered with functions at_time, at_fd_read, and at_fd_write rather than timer and wait_on_fd; the at_ convention will generalize better to future classes of events. Finally, tamed functions in Tamer are declared using code like tamed void f(), not tamed f().

The Tamer interface also differs substantially from that of Tame, which is distributed as part of sfslite.

AUTHOR

Eddie Kohler <kohler@seas.harvard.edu>
Based on joint work on Tame with Maxwell Krohn <krohn@mit.edu> and Frans Kaashoek <kaashoek@mit.edu>

SEE ALSO

tamer(1), tamer_fd(3)

Events Can Make Sense. Maxwell Krohn, Eddie Kohler, and Frans Kaashoek. In Proc. USENIX 2007 Annual Technical Conference. Also available at http://read.seas.harvard.edu/~kohler/pubs/krohn07events.pdf

The SFSlite libraries for writing asynchronous programs include the original Tame processor and libraries. The SFSlite libraries are larger and more full-featured than Tamer, but also harder to use. SFSlite is available at http://www.okws.org/doku.php?id=sfslite

Tamer home