In the case of these thread primitives, the <ucr/ucr.h> header file contains quite a bit of detail about how to properly use them. This section describes the basics and rational for what is there.
# include <ucr/ucr.h> # define UCR_STACK_ATTRIBUTE struct THREAD; extern THREAD* thread_create(void*stack, unsigned size, void (*fun)(void*), void*arg); extern void thread_runnable(THREAD*tid, bool front_flag = false); extern void thread_reschedule(); extern THREAD* thread_self();
The pointer to the opaque type THREAD represents a thread. All of the manipulation routines use this pointer to represent the parts of the thread, and internally they know how to break the thread into all its parts (including stack, registers, etc.)
The thread_self() function is the simplest, and returns the thread id of the currently running thread. When an application is first started there is only one thread, the main thread, and a call to thread_self() returns its id.
Eventually, most applications create other threads. This is done with the thread_create() function. The first two parameters pass to thread_create() a chunk of memory suitable for use as the stack of a new thread. uCR will take a piece of that memory to hold the parameters and state for the new thread, and will build an initial stack. The new thread is set up so that its first action when run is to call fun(arg) with the ``fun'' and ``arg'' passed.
Some processors or ports require more alignment then that of the base data type of the stack you are allocating. To express to gcc the required attributes of a stack, use the UCR_STACK_ATTRIBUTE macro like so:
# include <ucr/ucr.h> static long stack_space[128] __attribute__((UCR_STACK_ATTRIBUTE));
Most processors require at least long alignment so by all means use the UCR_STACK_ATTRIBUTE whenever you declare stack space. It is also best, by the way, to make the stack size an even multiple of 256 bytes. uCR puts some thread structures at the end of the stack space and an odd size stack space would destroy its alignment.
A newly created thread is ready to run, but is not started or in any ThreadQueue. The application may take the new thread and hold on to it for a while, or use any of the existing thread manipulation routines to schedule it for execution. One such routine is the thread_runnable() function.
thread_runnable() takes a thread and puts it on the end of the run queue, or the front of the run queue if front_flag is true. Generally, one would simply make the new thread runnable and leave it alone. It is still not running, but uCR will get around to it after all the threads in front of it give up the CPU.
thread_runnable() is the fundamental means of placing threads in the run queue. If a thread is not it the run queue, it is placed in the front or back. If a thread is already in the run queue, this function moves the thread to the front or back of the run queue.
The thread_reschedule() function tells uCR to immediately look at the run queue and change threads if needed to get the front thread running. Normally, the typical course of events leads the current thread to block, or an interrupt handler to wake up threads, and these will call thread_reschedule() implicitly. However, one might find cause to use it explicitly.
The following is an example implementation of a thread yield function using what you know so far:
# include <ucr/ucr.h> void example_yield() { thread_runnable(thread_self()); thread_reschedule(); }
Notice that thread_runnable(thread_self()) causes the current thread to be placed on the end of the run queue. The thread_reschedule() then causes uCR to suspend this thread in favor of any other threads in the run queue.
The actual implementation of thread_yield() is in fact very similar to this-there are only slight differences for the sake of optimization. As you see, these few functions, and the ThreadQueue class, are all that are needed for implementing cooperative threaded applications. However, a practical system also has interrupt handlers and preemption. The tools needed to handle this are described next.