Exercise 1

Thread creation and priorities

In this exercise, we will learn how to create and initialize two threads and learn how they can affect one another with their priorities. This is done using the K_THREAD_DEFINE() macro. This is the macro for defining and initializing a thread and plugging its data structures into the RTOS kernel, see the API below:

K_THREAD_DEFINE API

Exercise steps

1. In the GitHub repository for this course, open the base code for this exercise, found in lesson7/fund_less7_exer1 of whichever version directory you are using (v2.x.x or v1.6.0-v1.9.1).

Creating and initializing threads

2. Define the stack size and scheduling priority of the two threads that we will use when defining them.

#define STACKSIZE 1024
#define THREAD0_PRIORITY 7
#define THREAD1_PRIORITY 7

Even though the threads are simple in this exercise we are setting a stack size of 1024. Stack sizes should always be a power of two (512, 1024, 2048, etc.).

Note

In actual application development, you should choose the stack sizes more carefully, to avoid unnecessarily using the stack size. We do not need that here for a simple application.

We are giving the two threads the same priority. In this case, the actual number used isn’t important.

3. The thread entry functions for the two threads (thread0 and thread1) are provided for you but they contain no code. For now, let’s make them do something very simple which is just print a string in a while-loop.

Add the following printk() statement inside the thread0 and thread1 entry function. Make sure to change the name of the second thread to thread1.

printk("Hello, I am thread0\n");

Since the threads have no dependency on each other and neither yield nor sleep, they will always be in the “Runnable” state competing for the CPU resource.

4. Now that we have defined the necessary parameters, we can define the two threads, thread0 and thread1, using K_THREAD_DEFINE(). Pass the name, stack size, thread entry function, and priority to the macro, while the rest of the fields should either be NULL or 0, depending on the type.

K_THREAD_DEFINE(thread0_id, STACKSIZE, thread0, NULL, NULL, NULL,
		THREAD0_PRIORITY, 0, 0);
K_THREAD_DEFINE(thread1_id, STACKSIZE, thread1, NULL, NULL, NULL,
		THREAD1_PRIORITY, 0, 0);

The name can be anything but is used as the thread ID, so name wisely.

5. Build the application and flash it on your development kit. Using a serial terminal you should see the below output:

Notice that we only see output from thread0, indicating that thread1 never gets to run even though both threads were created with the same parameters and priority. The second thread is being starved, meaning it’s constantly being blocked since thread0 never yields, or waits (sleeps), or does any other event that triggers a rescheduling point.

Let’s see how we can fix this.

Thread yielding

To avoid starving thread1, we will make thread0 voluntarily yield using k_yield().

k_yield() causes the current thread to give away execution (yield) to another thread of the same or higher priority. The thread will be in the “Runnable” state, just pushed to the end of the list of “Runnable” threads. If there are no other threads in that list of the same or higher priority, the threads runs again immediately.

Note

To give lower priority threads a chance to run, the current thread needs to be put to “Non-runnable”. This can be done using k_sleep(), which we will see further on in this exercise.

A thread normally yields when it either has nothing else to do or wants to give other equal or higher priority threads a chance to run. There is usually some logic behind when a thread wants to call k_yield(). To keep this exercise simple, let us make thread0 yield every time it completes the printk() message.

6. Change the entry function of thread0 so it yields after running printk().

k_yield();

7. Build the application and flash it on your development kit. Using a serial terminal you should now see the below output:

Notice that thread0 is now able to print one message and then yields voluntarily to equal or higher priority threads. Since there is another equal priority thread in the “Runnable” state, this thread is now made active by the scheduler and will get the CPU time. Since thread1 never yields it will run forever once it becomes active, starving thread0.

8. Let’s make thread1 yield as well by adding k_yield() after it prints its message.

k_yield();

9. Build the application and flash it on your development kit. Using a serial terminal you should now see the below output:

And the events on the CPU look similar to the ones below

Timeline when both threads yield

Since both threads now yield after printing a message, the scheduler comes into play between every thread to evaluate if there are any runnable threads in the queue. Since both threads have equal priority, the scheduler will always choose the other thread as the next running thread, resulting in the two threads alternating each time they’ve run.

The disadvantage of this is that yielding this often and thereby invoking the scheduler also takes up CPU time. The scheduler uses CPU time to do the book-keeping of the kernel resources every time k_yield() is called which in turn costs power. A system with good architecture entails designing your threads so the scheduler uses a minimal amount of CPU time, i.e threads that have correct priorities and are reasonably considerate (yielding/sleeping/waiting) to other threads.

Thread sleeping

Since printing is a noncrucial task, it is acceptable for our threads to print less frequently. Therefore, a better option can be for the threads to sleep rather than yield. When sleeping, threads are put in the “Non-runnable” state and do very little processing. This is done by using k_sleep() or some variation, like k_msleep().

10. Let’s try this out by replacing the k_yield() function in both threads with k_msleep(5), i.e a sleep duration of 5 ms. Now the threads should look like below:

void thread0(void)
{
	while (1) {
           printk("Hello, I am thread0\n");
           k_msleep(5);
	}
}

void thread1(void)
{
	while (1) {
           printk("Hello, I am thread1\n");
	   k_msleep(5);
	}
}

11. Build the application and flash it on your development kit. Using a serial terminal you should now see the below output:

Although the output looks identical to when the threads were using k_yield(), there is a significant difference here. The threads are printing less frequently and thereby calling the scheduler less frequently. Take a look at the time sequence graph below and look at the idle period during which the system can switch to low power states.

Timeline when both threads sleep

As you can see, when both threads sleep for a certain amount of time and there are no other ready threads that can be made active by the scheduler, it makes the idle thread (which is one of the system threads) active.

In conclusion:

  • k_yield() will change the thread state from “Running” to “Runnable”, which means that at the next rescheduling point, the thread that just yielded is still a candidate in the scheduler’s algorithm for making a thread active (“Running”). The overall result is that after the thread yields, there will be at least one item in the runnable thread queue for the scheduler to choose from at the next rescheduling point.
  • k_sleep() will change the thread state from “Running” to “Non-runnable” until the timeout has passed, and then change it to “Runnable”. This means that the thread will not be candidate in the scheduler’s algorithm until the timeout amount of time has passed. Hence thread sleeping is better choice for adding delays and not for yielding.

The solution for this exercise can be found in the GitHub repository, lesson7/fund_less7_exer1_solution of whichever version directory you are using (v2.x.x or v1.6.0-v1.9.1).

Register an account
Already have an account? Log in
(All fields are required unless specified optional)

Forgot your password?
Enter your email address, and we will send a link to reset your password.