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:

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: 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:

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:

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:

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: 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