Best practice for spawning a (short) Zephyr thread from main and terminating it?

Hi, I am relatively new to the Nordic platform and Zephyr RTOS. I am using nRF Connect SDK version 2.6.0 (the VS Code extension on Windows 11) with the nRF52840DK. I was wondering what the best strategy would be for spawning threads that do a small task and then terminate. I have gone through the documentation here and managed to come up with a small demo program to test my understanding. In the program below, the main thread just periodically reads a button state and if high, spawns a new thread. This newly spawned thread just prints a message and returns, which according to the documentation will cause the thread to terminate. The main thread in the meantime sleeps for sometime, within which the thread will probably have terminated. I have flashed it to my nRF42840DK it is working.

/* Demo program: spawning and terminating Zephyr threads. */
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys/printk.h>

/* Define GPIO pin for collecting button input. */
#define BUTTON_NODE DT_ALIAS(sw0)
static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(BUTTON_NODE, gpios);

/* Define entry point for thread (target function). */
void button_pressed(void *, void *, void *)
{
    static uint32_t npressed = 0;
    printk("[%02" PRIu32 "] pressed!\n", ++npressed);
    /* Terminate thread: exit from here. */
}

/* Define thread params. */
#define MY_STACK_SIZE 500
#define MY_PRIORITY 5

/* Allocate stack for thread. */
K_THREAD_STACK_DEFINE(my_stack_area, MY_STACK_SIZE);

/* Allocate a Thread Control Block (TCB). */
static struct k_thread my_thread_data;

/* Function to be called from main thread. */
int main(void)
{
    /* Setup GPIO. */
    if (!gpio_is_ready_dt(&button)) {
        return 1;
    }
    gpio_pin_configure_dt(&button, GPIO_INPUT);

    /* Poll for button press every 100 ms and spawn thread if found. */
    for (;;) {
        int val = gpio_pin_get_dt(&button);
        if (val == 1) {
            /* Spawn thread. */
            k_thread_create(
                &my_thread_data, my_stack_area,
                K_THREAD_STACK_SIZEOF(my_stack_area),
                button_pressed, NULL, NULL, NULL,
                MY_PRIORITY, 0, K_NO_WAIT
            );
        }
        k_msleep(100); /* Enough time for the thread spawned above to exit. */
    }
    return 0;
}
 

I have a few questions:

1. is this the correct approach for spawning short-lived threads? Am I forgetting to do some cleanup? and are there better approaches to use cases such as this ?

2. is k_thread_join() needed here? Because the code seems to work fine without it.

I am a relatively inexperienced dev delving into the world of RTOS programming for the first time so suggestions are really appreciated. Thanks!

  • 1. is this the correct approach for spawning short-lived threads? Am I forgetting to do some cleanup? and are there better approaches to use cases such as this ?

    Creation : seems to be a good approach to create a thread in main like you have done. It looks like the thread is created in main without any condition, so when main enters, the thread for button_pressed is created,

    Termination :  Your thread button_pressed needs to wait for something or sleep until the condition for the thread termination.

    In Zephyr, a thread can terminate its execution in two ways:

    1. Termination: A thread can synchronously end its execution by returning from its entry point function. This is known as termination. A thread that terminates is responsible for releasing any shared resources it may own (such as mutexes and dynamically allocated memory) prior to returning, since the kernel does not reclaim them automatically.

    Here is an example of how a thread can terminate:

    void my_entry_point(int unused1, int unused2, int unused3)
    {
    while (1) {
    ...
    if (<some condition>) {
    return; /* thread terminates from mid-entry point function */
    }
    ...
    }
    
    /* thread terminates at end of entry point function */
    }

    2. Aborting: A thread may asynchronously end its execution by aborting. The kernel automatically aborts a thread if the thread triggers a fatal error condition, such as dereferencing a null pointer. A thread can also be aborted by another thread (or by itself) by calling `k_thread_abort()`. However, it is typically preferable to signal a thread to terminate itself gracefully, rather than aborting it.

    Here is an example of how a thread can be aborted:

    k_thread_abort(thread_id);
    

    In both cases, once a thread has terminated, the kernel guarantees that no use will be made of the thread struct. The memory of such a struct can then be re-used for any purpose, including spawning a new thread.

  • seems to be a good approach to create a thread in main like you have done. It looks like the thread is created in main without any condition, so when main enters, the thread for button_pressed is created,

    I'm a little confused, sorry. I'm under the impression that the thread will be spawned only when k_create_thread() is called, which is enclosed within the conditional if (val == 1)

    In my case, I am terminating without any condition and there's no cleanup since I don't have any mutexes, kernel objects or malloc-allocated memory. So I think I'm good on that part, right?

    Actually I am going to be adapting this code for my application in which a newly spawned thread will pulse a GPIO pin connected to a vibration motor (for a haptic feedback), on button press. So the same application logic as here, except button_pressed() will have some gpio_pin_set_dt()  and k_msleep() calls. Do you have any suggestions on what priority I could assign to such a thread? I know that the main thread has priority 0. For now, I'm using priority 5.

  • nik2k said:
    I'm a little confused, sorry. I'm under the impression that the thread will be spawned only when k_create_thread() is called, which is enclosed within the conditional if (val == 1)

    My bad, yes, you have covered that.

    Also, one small observation. It looks like you have to consider that the user might do a long button press longer than 100ms in which case the thread will be created, run and aborted more than once for one long button press. Just saying that you might need to calibrate the k_sleep time not just for spawned thread to exit but also for long button presses.

  • Thanks. My app will actually be using interrupts for handling double and long presses (using Zephyr timers). Once the presses are detected, GPIO pulses are ignored and the vibration-motor-thread is spawned. So the case that you mentioned has been covered.

Related