TASK(3C++)

NAME
task - coroutines in the C++ task library

SYNOPSIS

#include <task.h>

typedef int (*PFIO)(int,object*);
typedef void (*PFV)();
class object {
public:
	 // exported constants and types
	enum objtype { OBJECT, TIMER, TASK, QHEAD, QTAIL, INTHANDLER };

	// exported constructor
	object();

	 // exported data members
	object*	o_next;
	static	PFIO error_fct;

	// exported virtual functions
	virtual	objtype	o_type();
	virtual	int	pending();
	virtual	void	print(int, int=0);
	// print flags, used as arguments to print function
	#define CHAIN		1
	#define VERBOSE	2
	#define STACK		4

	// exported misc functions
	void	alert();
	void	forget(task*);
	void	remember(task*);

	// exported static member functions
	static	int task_error(int, object*);
	static	task* this_task();
};
class sched: public object {
public:
	// exported constants and types
	enum statetype { IDLE=1, RUNNING=2, TERMINATED=4 };

protected:
	// protected constructor
	sched();

public:
	// exported data members
	static	task*	clock_task;
	static	PFV	exit_fct;

	// exported virtual functions
	virtual	int	pending();
	virtual	void	print(int, int =0);
	virtual	void	setwho(object*);

	// exported misc functions
	void	cancel(int);
	int	dont_wait();
	sched*	get_priority_sched();
	int	keep_waiting();
	statetype	rdstate();
	long	rdtime();
	int	result();

	// exported static member functions
	static	long	get_clock();
	static	sched*	get_run_chain();
	static	int	get_exit_status();
	static	void	set_exit_status(int i);
	static	void	setclock(long);
};
class task: public sched {
public:
	// exported constants and types
	typedef unsigned char _Uchar;
	enum modetype { DEDICATED=1, SHARED=2 };
	#define DEFAULT_MODE DEDICATED
	#define SIZE 3000

protected:
	// protected constructor
	task(char* = 0, modetype = DEFAULT_MODE, int = SIZE);

public:
	// exported data members
	task*	t_next;
	_Uchar*	t_name;

	// exported virtual functions
	virtual	objtype	o_type();
	virtual	void	print(int, int =0);
	virtual	void	setwho(object*);

	// exported misc functions
	void	cancel(int);
	void	delay(int);
	int	preempt();
	void	resultis(int);
	void	sleep(object* =0);
	void	wait(object*);
	int	waitlist(object* ...);
	int	waitvec(object**);
	object*	who_alerted_me();

	// exported static member function
	static	task*	get_task_chain();
};
class timer: public sched {
public:
	// exported constructor
	timer(int);

	// exported virtual functions
	virtual	objtype	o_type();
	virtual	void	print(int, int =0);
	virtual	void	setwho(object*);

	// exported misc functions
	void	reset(int);
};

DESCRIPTION

A task is an object with a control thread running its associated program. More specifically, a user task is an object of a type derived from class task. The constructor of that object is the main program of the task. The task does not survive the completion of the constructor.

The task system works in terms of operations which may be performed immediately or which must wait, and objects which are ready or pending. If a task requests a service which may be performed immediately, or waits on an object which is ready, the task continues execution. If the service cannot be performed immediately or if the object being waited on is not ready, the task is blocked. When a running task is blocked, the scheduler selects the next task which is ready to run and gives it control. A task is never preempted, but may be blocked, thus allowing other tasks to run. Control proceeds in a round-robin sequence. (It is possible for a task to be given priority and moved to the head of the list of tasks to be run.) Whenever an object becomes newly ready, any tasks which were waiting for it are notified, and those tasks become ready to run.

A task may be in one of three states:

RUNNING
    Running now or ready to be run.

IDLE
    Not ready to run; waiting for some object.

TERMINATED
    The task has finished and cannot be resumed. The task's return value may be retrieved.

The task system is rooted in class object. Anything derived from this class may be put on a queue or waited on. Virtual member function pending() returns non-zero if the object is not ready. Each type of object may have its own version of this function, and thus its own criteria for determining whether it is ready or pending, but there may be only one such function.

Each object has a list of objects which are waiting on it, the remember chain. When a task waits on an object which is not ready, the task is suspended (IDLE) and added to the object's remember chain. When the object next becomes ready (not pending), each waiting task is notified via member function object::alert(). This function changes the state of these tasks to RUNNING and puts them back on the list of ready tasks, the scheduler's run chain.

Class sched, derived from object, provides the functionality common to task-like objects. The scheduling functions are provided here. Rather than a separate scheduler, the scheduling operations are part of class sched, and tasks cooperatively provide scheduling by means of its member functions. This class may not be instantiated, but may be used only as the base class of tasks and timers.

Class sched also provides the facilities for the simulated passage of time. Units of simulated time need have nothing to do with real time. The simulated system clock is initialized to zero, and can be set at most once via sched::setclock(). Thereafter, the clock advances only by calls to task::delay(), whose parameter is the number of simulated time units to delay. When that amount of simulated time has elapsed, delay() returns. The call to delay() causes the clock to advance to the earlier of the amount of delay and the next simulated time at which something is scheduled to happen. The current value of the clock may be read via function sched::getclock().

Class timer is a stripped-down task which simply delays the specified amount of simulated time, then terminates. A timer may be waited on, and when its time has elapsed any waiting tasks are alerted. A timer can be either RUNNING or TERMINATED, but never IDLE.

Class task is derived from sched and provides the basic functionality of user tasks. No object of type task may be instantiated. All user tasks must be derived directly from class task; that is, you may not further derive from a user task type. Examples:
    task mytask;    // ERROR: cannot create a task object
    class consumer : public task { ... }    // OK
    class glutton : public consumer { ... }    // ERROR: cannot derive again
The first error is diagnosed by the compiler, since the task constructor is protected. The error in the third line cannot be diagnosed, but the program will not work.

A task may not terminate by simply exiting the constructor, nor may the function return mechanism be used to return a value from a task. A task must instead set its return value by calling either task::resultis() or task::cancel(). This puts the task in the TERMINATED state. Other tasks may retrieve the result value by calling task::result(), which will suspend the calling task until the queried task has terminated. Even if a task does not need to return a value, it must call resultis() or cancel() to ensure the task is properly terminated before it is destroyed.

The task constructor takes three optional arguments: a name, a mode, and a stack size.

The name should be a character string allocated statically or on the heap, not a character array local to some function. The name appears only in printouts, and has no effect on the task operation.

The mode specifies what sort of stack the task will use. The mode may be DEDICATED (the default) or SHARED. Usually you want a dedicated stack, meaning the task has a reserved area of memory for its own stack. Shared stacks are expensive, because tasks may need to swap their own stacks between the shared area and a save area as they become active and inactive. If you have hundreds of small tasks, it makes sense for them to share stack space.

The stack size argument specifies the maximum amount of stack space the task may use. The default is 3000 bytes. There is no foolproof test for overflowing the stack space, and the stack cannot be dynamically expanded. The stack size is checked when a task is swapped in, and overflow is a fatal error. If the stack overflows during execution due to depth of function call nesting and amount of local variable storage, this may cause bizarre program behavior or a crash.

When you derive a class from task, its constructor becomes a new task which runs along with others which may already exist. Function main() becomes a task with the creation of the first task in the system. If several tasks are started sequentially in main(), the second task cannot be created until the first becomes blocked and main() regains control to create a new task, and so on for additional tasks. This also means that main() must invoke resultis() or cancel() after starting all its tasks, so that it doesn't exit and terminate the entire program while tasks are running.

Any task constructor may itself create new tasks.

  Class object

Class object has only one constructor, which takes no arguments.

object o;
    Constructs an object o (which is illegal, since the constructor is protected), which is not on any list.

object *p = o.o_next;
    An object may be on at most one list (run chain, queue, etc) at at time. The next object on the list is pointed to by public data member o_next. This member will be zero if there is no next object, and, in particular, if the current object is not on any list.

objtype t = p->o_type();
    This virtual function returns the enumeration constant of type objtype corresponding to the type of object which p points to.

int i = p->pending();
    Returns non-zero (true) if the object pointed to by p is not ready. Derived classes must define a suitable version of this virtual function if they are to be waited on. The default version in class object always returns true (not ready). The other predefined classes have their own versions of this function.

o.print(how);
    Prints basic information about the object pointed to by p on stdout (not cout). If the first int parameter, how, has bit VERBOSE set, prints information about all the tasks on the object's remember chain. The second argument is for internal use and defaults to zero. This virtual function is normally called by the print() functions from derived classes.

o.alert()
    Scans the remember chain of task o, changes each task from IDLE to RUNNING mode and puts it on the run chain. When a pending object is waited on, the waiting task is made IDLE, taken off the run chain, and put on the object's remember chain. When the object becomes no longer pending, this function is called automatically by the predefined classes to wake up all the waiting tasks.

o.forget(p)
    Removes all instances of the task pointed to by p from the remember chain of task o.

o.remember(p)
    Adds the task pointed to by p to the remember chain of task o.

int user_err_func(int, object*);
    object::error_fct = user_err_func;
    Allows the user a measure of control over error recovery. If a user error function is defined, it must take an int parameter which is one of the error numbers in the DIAGNOSTICS section below, and a pointer to an object. The latter will be the object which called function object::task_error(), or which is otherwise responsible for the error, as explained below. Static data member object::error_fct may be set to point to such a user error function. If it is set, task_error() will call the user function. If the user function returns zero, task_error() will return, and the operation which resulted in an error will presumably be retried. If the user function returns non-zero, task_error() will call exit().

int i = o.task_error(err, p);
    int i = object::task_error(err, p);
    This static member function is called by task system functions when a run-time error occurs. The first int parameter is one of the error codes listed in the DIAGNOSTICS section below. The second parameter is intended to be the object which called the function, or may be zero if there is no identifiable object responsible. If static data member error_fct is zero, task_error() prints a message on stderr (not cerr) and calls exit(err). If error_fct is non-zero, the user function is called with parameters err and p. If the user function returns 0, then so does task_error() . Otherwise, it calls exit(err) as above. If this function returns 0, its caller is intended to retry the operation which failed.

task *tp = o.this_task();
    task *tp = object::this_task();
    This static member function returns a pointer to the task which is currently running.

  Class Sched

Class sched provides the basic scheduling functions, and classes task and timer are each derived from it. No object of this class may be created; it serves only as a base class.

sched s;
    Constructs an object s of type sched (which is illegal, since the constructor is protected). The constructor has no arguments. The object is initialized to be IDLE and have no delay.

task* tp = ...;
    sched::clock_task = tp;
    If static data member sched::clock_task is non-zero, the task it points to will be scheduled before any other task each time the clock advances. When the current task, if any, is blocked, this clock task will be resumed. The clock task must be IDLE when it is resumed, and may place itself in that state by calling task::sleep().

void user_exit_func(void);
    sched::exit_fct = user_exit_func;
    You may declare a function taking no parameters and returning void and assign it to static data member sched::exit_fct. This function will then be called just before final exit from the task system.

int i = s.pending();
    The version of this function for class sched returns non-zero (true) if the object (a task or timer) is TERMINATED, and zero (false) otherwise.

s.print(how);
    Prints data about the sched portion of the object on stdout (not cout). The first int parameter, how, is passed to object::print(). The second argument is for internal use and defaults to zero. This virtual function is normally called by the print() functions from derived classes.

tp->setwho(objp)
    This virtual function is provided for classes task and timer. See the writeup for those classes. It is an error to call this version.

s.cancel(val);
    Puts task or timer s in the TERMINATED state, and uses int value val as its returned value. Unlike resultis(), cancel() does not suspend the caller. This enables one task to terminate another while retaining control.

int i = s.dont_wait();
    Returns the difference between the number of times keep_waiting() and the number of times dont_wait() have been called, not counting this call. The count is then decremented. This count is kept in a static variable, so it represents the system-wide difference. The count is supposed to represent the number of objects which are waiting for external events. See keep_waiting() below.

sched *tp = s.get_priority_sched();
    A special system task, the interrupt alerter, is priority-scheduled when a signal has occurred which was being waited for. This function returns a pointer to that task if it has been scheduled, zero otherwise. See also interrupt(3C++).

int i = s.keep_waiting();
    Returns the difference between the number of times keep_waiting() and the number of times dont_wait() have been called, including this call. That is, the count is first incremented. This count is kept in a static variable, so it represents the system-wide difference. The count is supposed to represent the number of objects which are waiting for external events. In particular, the constructor for class Interrupt_handler calls keep_waiting() and the destructor calls dont_wait(). The scheduler uses this information so the task system doesn't exit when interrupt handlers are active.

statetype t = s.rdstate();
    Returns the state of task or timer s: RUNNING, IDLE, or TERMINATED.

long l = s.rdtime();
    Returns the simulated time at which task or timer s is scheduled to run.

int i = s.result();
    Returns the ``result'' of task s, which is set by sched::cancel(), task::cancel(), or task::resultis(). If task s has not terminated, the calling task will be suspended until it does terminate. It is an error for a task to call result() on itself.

long l = s.getclock();
    long l = sched::getclock();
    Returns the value of the current simulated time.

sched* p = s.get_run_chain();
    sched* p = sched::get_run_chain();
    Returns a pointer to the run chain, the list of all tasks and timers which are ready to run. These objects are linked through the o_next field.

int i = s.get_exit_status();
    int i = sched::get_exit_status();
    When the task system exits normally (not because of a call to task_error()), the value last given to set_exit_status() is passed to the system exit() routine. This is by default zero, the indicator of succesful completion. Function get_exit_status() returns the value currently slated to be passed to exit().

s.set_exit_status(i);
    sched::set_exit_status(i);
    The int value i is saved, and will be passed to the system function exit() if the task system terminates normally (not because of a call to task_error()). If set_exit_status() is never called, that value will be zero, the indicator of succesful completion. Otherwise, the last value passed to set_exit_status() will be used.

s.setclock(l);
    sched::setclock(l);
    Sets the simulated clock to long value l. System time starts at zero by default. It is an error to call this function more than once.

  Class Task

task t(name, mode, size);
    Constructs a task. The parameters are described above. As also noted above, the constructor is protected, and thus task may serve only as a base class for user tasks.

task *tp = t.t_next;
    The next task on the master list of all tasks. See get_task_chain() below.

unsigned char* p = t.t_name;
    A pointer to the name of the task as assigned by the task constructor.

objtype o = t.o_type();
    This virtual function defined in object returns the kind of object. For a task, the object kind is TASK.

t.print(how);
    Prints data about task t on stdout (not cout). The first int parameter, how, may have any combination of the VERBOSE and CHAIN bits set. If VERBOSE is set, addtional information is printed. If CHAIN is set, information on every task in the system is printed. This parameter is passed to sched::print(). The second argument is for internal use and defaults to zero.

t.setwho(objptr);
    This virtual function remembers the object pointed to by objptr as the object which alerted task t. The intent is that task t was waiting on object *objptr, and when it went from pending to ready, the object alerted this task and changed its state from IDLE to RUNNING. The object which was remembered may be retrieved with who_alerted_me().

t.cancel(val);
    Puts task t in the TERMINATED state, and uses int value val as its returned value. Unlike resultis(), cancel() does not suspend the caller. This enables one task to terminate another while retaining control. See also resultis().

t.delay(n);
    Suspends task t for n simulated time units, leaving it RUNNING. The RUNNING task with the least delay left gets control, and the simulated clock is advanced to its continuation time. When the clock has advanced by n units, task t will continue execution. The use of delay() is the only way to advance the clock.

int i = t.preempt();
    Suspends RUNNING task t, making it IDLE. It returns the number of time units left before t was scheduled to continue execution. It is an error to call preempt() for an IDLE or TERMINATED task.

t.resultis(val);
    Puts task t in the TERMINATED state, and uses int value val as its returned value. This value may be examined by calling t.result(). Any tasks which have called result() on task t and are thus waiting for the task to terminate will be alerted. A task may not return a value via the ordinary function return mechanism, but must call either resultis() or cancel() to terminate. Every task is pending until it is terminated.

t.sleep(objptr);
    Unconditionally suspends task t, making it IDLE. The optional argument points to an object that will ``remember'' task t and alert it when the object becomes ready. Unlike wait(), sleep() does not first check the object to see whether it is pending; the task is always suspended.

t.wait(objptr);
    Suspends task t (makes it IDLE) if the object pointed to by objptr is pending. In this case, t will be alerted by the object (made RUNNING and put back on the run chain) when the object becomes ready. If the object is not pending, task t is not suspended, but retains control.

int which = t.waitlist(op1, op2, ... , NULL);
    This function takes a list of pointers to objects, terminated by a null pointer. If all of the objects pointed to are pending, task t is suspended until one of them becomes ready. Waitlist returns when at least one of the tasks is ready, returning the index (starting from 0) in the list of objects that caused the return. Other objects in the list might also be ready at this point. If none of the objects are pending as of the call to waitlist(), task t is not suspended and waitlist() returns immediately.

int which = t.waitvec(objarray);
    This works in exactly the same way as waitlist(), except that this function takes an an array of pointers to objects instead of a variable-length list. The array is terminated by a null pointer.

task *tp = t.get_task_chain();
    task *tp = task::get_task_chain();
    This static member function returns the head of the list of all tasks. Every task in the system, RUNNING, IDLE, and TERMINATED, is on this list, linked via the t_next data member.

  Class Timer

timer tm(d);
    Constructs a timer which will expire in d simulated time units. The timer is placed on the run chain.

objtype o = tm.o_type();
    This virtual function defined in object returns the kind of object. For a timer, the object kind is TIMER.

tm.print(how);
    Prints data about timer tm on stdout (not cout). The first int parameter, how, is passed to sched::print(). The second argument is for internal use and defaults to zero.

tm.setwho(objptr);
    This virtual function has no effect for timers, since they cannot be alerted.

tm.reset(d);
    Resets the delay of timer tm to d simulated time units. A timer may be reset even if it has been terminated. This means that a timer may be reused; they need not be continually created and destroyed.

DIAGNOSTICS

When the task system detects a run time error, it calls object::task_error() as described above. The following table lists the possible error values, associated messages, and meanings.

Error Name | Message | Explanation
E_ERROR | (no message) | Undefined error.
E_OLINK | object::delete(): has chain | Attempt to destroy an object which ``remembers'' one or more objects.
E_ONEXT | object::delete(): on chain | Attempt to destroy an object which is on a list.
E_GETEMPTY | qhead::get(): empty | Attempt to get from an empty queue.
E_PUTOBJ | qtail::put(): object on other queue | Attempt to put an object on a queue which is already on a queue.
E_PUTFULL | qtail::put(): full | Attempt to put an object on a full queue.
E_BACKOBJ | qhead::putback(): object on other queue | Attempt to put back onto a queue an object which is already on a queue.
E_BACKFULL | qhead::putback(): full | Attempt to put back an object to a full queue.
E_SETCLOCK | sched::setclock(): clock!=0 | Attempt to set the clock twice.
E_CLOCKIDLE | sched::schedule(): clock_task not idle | The clock task was not IDLE when it was scheduled to be run.
E_RESTERM | sched::insert(): cannot schedule terminated sched | Attempt to resume a TERMINATED task.
E_RESRUN | sched::schedule(): running | Attempt to resume a RUNNING task.
E_NEGTIME | sched::schedule(): clock<0 | Attempt to set the clock to a negative value or use a negative delay.
E_RESOBJ | sched::schedule(): task or timer on other queue | Attempt to resume task already on a queue.
E_HISTO | histogram::histogram(): bad arguments | Inconsistent or illegal arguments to histogram constructor.
E_STACK | task::task() or task::resume(): stack overflow | The run time task stack has overflowed.
E_STORE | new: free store exhausted | No more free store available for task bookkeeping.
continued...

Error Name | Message | Explanation
E_TASKMODE | task::task(): bad mode | Illegal mode arguments for the task constructor.
E_TASKDEL | task::~task(): not terminated | Attempt to destroy a non-TERMINATED task.
E_TASKPRE | task::preempt(): not running | Attempt to preempt a non-RUNNING task.
E_TIMERDEL | timer::~timer(): not terminated | Attempt to destroy a non-TERMINATED timer.
E_SCHTIME | sched::schedule(): runchain corrupted: bad time | Run chain corrupted, not in time order.
E_SCHOBJ | sched object used directly (not as base) | Attempt to use a sched object rather than a derived task or timer.
E_QDEL | queue::~queue(): not empty | Attempt to destroy a non-empty queue.
E_RESULT | task::result(): thistask->result() | A task attempted to call result() on itself.
E_WAIT | task::wait(): wait for self | A task attempted to wait on itself.
E_FUNCS | FrameLayout::FrameLayout(): function start | (not used)
E_FRAMES | FrameLayout::FrameLayout(): frame size | (not used)
E_REGMASK | task::fudge_return(): unexpected register mask | (not used)
E_FUDGE_SIZE | task::fudge_return(): frame too big | (not used)
E_NO_HNDLR | signal_handler - no handler for signal | A signal occurred but no handler was registered.
E_BADSIG | illegal signal number | Attempt to register an illegal signal number.
E_LOSTHNDLR | Interrupt_handler:: ~Interrupt_handler(): signal handler not on chain | Internal error: a signal handler was lost.
E_RUNCHAIN | sched::sched(): run chain empty | All tasks have terminated and there are no interrupt handlers.

NOTE
As of the date of this release, the coroutine library will not be supported beyond the current version.

SEE ALSO
TASK.INTRO(3C++), interrupt(3C++), queue(3C++), tasksim(3C++), exit(3), Task Library Tutorial