Cheap Threads: Tutorial
The following outlines the steps needed to use Cheap Threads. For
more details, see the documentation for the relevant functions.
Most of the Cheap Threads functions return an int, which may be either
CT_OKAY (indicating success) or CT_ERROR (indicating failure). Some
of the exceptions are simple functions that have no way to fail, and
therefore return void.
Preliminaries
Before you start running the threaded portion of your application,
you must install at least one active thread, by calling
ct_create_thread(). Otherwise the scheduler will have nothing to
schedule. Once the scheduler is running, your threads may create
other threads dynamically as needed.
Other preliminaries are optional:
-
Install one or more inactive threads by calling
ct_create_sleeping_thread(). Such a thread will remain asleep
until it is awakened by an event, such as a message from another
thread.
-
Subscribe to one or more message types on behalf of one or more of
your threads.
-
Install an error message handler by calling ct_install_error_reporter().
By default, Cheap Threads writes error messages to standard error.
Since this treatment may not be appropriate in some environments, you
can specify a callback function to deliver error messages as you see
fit.
-
Install user exits that the scheduler will invoke just before
or just after invoking each thread. Use ct_install_pre_function()
or ct_install_post_function(), respectively. You can install
either kind of user exit or both.
-
Call ct_set_countdown() to adjust the strength of priorities in the
scheduling algorithm. See the section on Tweaking the Scheduler,
below.
Typically you would call these functions before starting the
scheduler, but you can also call them from within a thread.
Installing a Thread
The ct_create_thread() function defines a thread to the scheduler and
makes it available to be invoked. It optionally populates a Ct_handle,
a struct that your code can use to identify the new thread. Your own
code should not access the contents of a Ct_handle, because they may
change in future releases.
The ct_create_sleeping_thread() function is identical to
ct_create_thread(), except that it does not enqueue the new thread
for execution. A sleeping thread doesn't run until it is awakened
by the arrival of a message, or by an explicit request to activate
it.
When creating a thread you define its priority level. You must also
supply a pointer to a callback function that the scheduler will call
when invoking the thread.
You may optionally provide a void pointer to arbitrary data. For
example, you might dynamically allocate and initialize a chunk of
memory to be used exclusively by that thread, and install a pointer
to that memory.
You may also supply a pointer to a cleanup function for the scheduler
to call when the thread expires, for deallocating memory or other
resources owned by the thread. Such a function is similar to a
destructor in C++.
You must create at least one thread before calling the scheduler.
Once the scheduler is running, you can create more threads as needed
from within previously created threads.
Suppose you create one or more threads, but then decide not to run the
scheduler after all, probably because of an error condition. You can
call ct_clear() to clear out the scheduler's internal data structures.
It will also call the cleanup function for each thread. There is no
other way to free the memory allocated for threads without running the
scheduler.
Running the Scheduler
Once you have created at least one thread, you can run the scheduler
by calling ct_schedule(). It runs until one of the following happens:
- A thread calls ct_halt().
- There are no runnable threads left.
- A thread returns something other than CT_OKAY to the scheduler.
- A thread (or the Cheap Threads software itself) calls
ct_fatal_error().
- Disaster happens -- a bus error, a user interrupt, thermonuclear
attack, and so forth.
Before it returns, ct_schedule() calls the cleanup function for any
remaining threads, thereby destroying all threads. If you want to call
ct_schedule() again you'll have to start over from the beginning. You
can't resume where you left off. If necessary, it should be possible
to extend Cheap Threads by adding ct_pause() and ct_resume() functions.
You cannot nest Cheap Threads. If you call ct_schedule() from within
a running thread, it will fail with a fatal error.
Running a Thread
When it invokes a thread, the scheduler passes it the same void
pointer that you specified when you created the thread. If this
pointer is NULL, the thread need do nothing with it. Otherwise the
thread may dereference it with an appropriate cast and thereby access
its own data. The thread may obtain the same pointer by calling
ct_self_data().
Casting and dereferencing a void pointer is always hazardous, because
the compiler can't protect you against type blunders. It is up to you
to ensure that the type of the cast matches the type of the data.
When it is finished, the thread should return CT_OKAY or CT_ERROR to
the scheduler -- either as a return value or as the argument to the
ct_return function. If it returns or passes anything but CT_OKAY, the
scheduler will halt immediately.
Before returning, the thread may call any of the following:
-
ct_penalize() -- temporarily lowers the thread's priority. See
Tweaking the Scheduler, below.
-
ct_exit() -- tells the schedule that this thread is ready to expire.
When the thread returns, the scheduler will call the thread's cleanup
function, if there is one, and destroy the thread.
-
ct_wait() -- tells the scheduler to put this thread to sleep. The
thread will not run again until another thread sends it a message.
-
ct_halt() -- tells the scheduler to terminate normally. After the
thread returns, the scheduler will destroy all threads, calling
whatever cleanup functions are defined for them, and return CT_OKAY
(assuming that no error occurs in the meanwhile).
-
ct_fatal_error() -- notifies the scheduler of a fatal error. After the
thread returns, the scheduler will destroy all threads, calling whatever
cleanup functions are defined for them, and return CT_ERROR.
Tweaking the Scheduler
Since the scheduler runs high-priority threads more often than
low-priority threads, the most important way to affect the scheduling
is by setting the priorities of the threads.
You can use the ct_set_countdown() function to adjust the strength
of the prioritization. At one extreme, a low priority thread will
almost never run, except when another thread sends it a message. At
the other extreme the scheduler will work more like a round robin.
For a true round robin you must give each thread the same priority.
By calling the ct_penalize() function, a thread may temporarily
give itself a lower priority. After the next run it reverts to the
priority with which it was created. For example, a thread which
performs a time-consuming operation may penalize itself in order to
give other threads a chance to make up for lost time.
To use these functions effectively, it will be helpful to have a
fuller understanding of how the scheduler works internally, as
described elsewhere.
If these functions aren't enough to balance the demands of your
various threads, you may be able to devise other tricks according to
the peculiarities of your application. For example:
-
Install user exits for tasks which must be performed more frequently
than anything else.
-
Install two or three threads which share the same state and do the
same job. The effect will be approximately like calling the same
thread two or three times as often.
-
Make a thread do more work, or less, on each invocation.
Nevertheless, the kind of cooperative multitasking implemented by
Cheap Threads will always be somewhat imprecise in its scheduling.
Even if you seem to have found a good balance, you may disrupt that
balance when you move the application to a platform with a different
CPU or different IO devices. If you need fine control over the way
you allocate CPU time among your tasks, then Cheap Threads is
probably not for you.
Communication Among Threads
Since all threads share the same address space, they can readily
communicate through the memory they share. There is no need to
protect shared memory or other shared resources with mutexes,
semaphores, critical sections, or other mechanisms. Therefore the
sharing of memory and other resources is much simpler than it would be
with true multithreading.
Threads can also send messages to each other. This mechanism is
more complicated than the use of shared memory, but it has at least
two advantages:
- A thread can put itself to sleep until a message arrives, or until
another thread wakes it up. That way it doesn't clutter up the
scheduler when it doesn't have anything to do.
-
A message may be sent to a specific thread, as identified by a thread
handle. That kind of specificity would be awkward to achieve through
the use of shared memory alone, especially when the application creates
threads dynamically in unpredictable ways.
Each message is assigned a type. Sometimes the type is all the
recipient needs to know, but if it isn't, a message can also contain
additional data, as defined by the application.
As noted above, the sender may address a message to a single specific
thread. It may also distribute a message to all threads of a given
class, namely those that have subscribed to messages of that type.
Finally, it may broadcast a message to all threads, including itself.
Similar options are available if you want to enqueue one or more
threads for execution but you don't need to send them any data. This
mechanism is equivalent to the passing of empty messages, but simpler
and more efficient.
Messages are discussed in more detail
elsewhere.
Miscellaneous Functions
Three utility functions are useful for keeping track of thread handles:
-
ct_self() -- returns a handle referring to the currently
running thread.
-
ct_valid_handle() -- determines whether a handle refers to a valid
thread. In particular, it determines whether a previously valid thread
has expired.
-
ct_same_thread() -- determines whether two handles refer to the same
thread.
The ct_self_data() function returns a void pointer to the private
memory of the current thread -- the same void pointer that is passed
to the thread when it receives control. This function is not strictly
necessary but may be convenient when the thread makes deeply
nested function calls.
The ct_report_error() function issues a specified error message. By
default, it adds a newline to the message and writes it to standard
error. In Embedded Cheap Threads, since standard error may not be
available, the default behavior is to do nothing. In either case, you
can customize the issuance of error messages by using the
ct_install_error_reporter() to install your own callback function.
Home