在本练习中,我们将学习如何创建和初始化两个线程,并了解它们的优先级如何相互影响。在 Zephyr 中有两种创建线程的方法,第一种是通过动态方式(在运行时)使用 k_thread_create() 而另一种更常用的方法是使用 K_THREAD_DEFINE() 宏以静态方式(在编译时)创建。这是用于定义和初始化线程并将其数据结构插入 RTOS 内核的宏,其 API 如下所示:

K_THREAD_DEFINE API1. 在本课程的 GitHub 仓库中,打开此练习的基础代码,位于 l7/l7_e1。
创建并初始化线程
2. 定义我们在定义这两个线程时将使用的堆栈大小和调度优先级。
#define STACKSIZE 1024
#define THREAD0_PRIORITY 7
#define THREAD1_PRIORITY 7C尽管本练习中的线程很简单,但我们仍将堆栈大小设置为 1024。堆栈大小应始终为 2 的幂(512、1024、2048 等)。
在实际应用开发中,您应该更仔细地选择堆栈大小,以避免不必要的堆栈空间占用。对于这个简单的应用程序,我们不需要那样做。
我们为这两个线程赋予相同的优先级。在这种情况下,使用的具体数值并不重要。
3. 已为您提供了这两个线程的线程入口函数(thread0 和 thread1),但它们不包含任何代码。现在,让我们让它们执行一些非常简单的操作,即在 while 循环中打印一个字符串。
在 thread0 和 thread1 入口函数中添加以下 printk() 语句。请务必将第二个线程的名称更改为 thread1。
printk("Hello, I am thread0\n");C由于线程之间没有依赖关系,既不让出也不睡眠,因此它们将始终处于“可运行”状态,竞争 CPU 资源。
4. 现在我们已经定义了必要的参数,可以使用 K_THREAD_DEFINE() 来定义这两个线程 thread0 和 thread1,。传递要创建的线程名称、其堆栈大小、线程入口函数、启动时传递给线程的可选参数(最多三个)、线程优先级、可选的线程选项,以及最后的可选调度延迟。
对于传递给线程的可选参数,我们将传递 NULL。
对于可选的线程选项,它可以允许我们进行配置,例如,是否将线程视为基本线程 K_ESSENTIAL。它指示内核将线程的终止或中止视为致命系统错误。默认情况下,线程不被视为基本线程。在本练习中,我们将不设置任何选项。
对于可选的调度延迟,我们也将其设置为零,这意味着线程将被创建并立即置于就绪状态。
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);C名称可以是任何内容,但会被用作线程 ID,因此请明智命名。
5. 构建应用程序并将其烧录到您的开发套件中。使用串口终端,您应该看到以下输出:
*** Booting nRF Connect SDK ***
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0
Hello, I am thread0Terminal请注意,我们只看到来自 thread0 的输出,这表明 thread1 从未得到运行,即使两个线程都是使用相同的参数和优先级创建的。第二个线程正处于饥饿状态,这意味着它一直被阻塞,因为 thread0 从不让出、等待(睡眠)或执行任何其他触发重新调度点的事件。
让我们看看如何解决这个问题。
线程让出
为了避免 thread1 饥饿,我们将使 thread0 使用 k_yield() 主动让出。
k_yield() 会导致当前线程将执行权让给(yield)另一个具有相同或更高优先级的线程。该线程将处于“可运行”状态,只是被推到了“可运行”线程列表的末尾。如果该列表中没有其他相同或更高优先级的线程,则该线程会立即再次运行。
为了给较低优先级的线程运行机会,当前线程需要被置于“不可运行”状态。这可以使用 k_sleep() 来实现,我们将在本练习的后续部分看到。
线程通常在没有其他事情可做,或者想要给其他同等或更高优先级的线程运行机会时让出。线程何时想要调用 k_yield() 通常有一定的逻辑。为了保持本练习简单,让我们让 thread0 在每次完成 printk() 消息后让出。
6. 更改 thread0 的入口函数,使其在运行 printk() 后让出。
k_yield();C7. 构建应用程序并将其烧录到您的开发套件中。使用串口终端,您现在应该看到以下输出:
*** Booting nRF Connect SDK ***
Hello, I am thread0
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1
Hello, I am thread1Terminal请注意,thread0 现在能够打印一条消息,然后主动让出给同等或更高优先级的线程。由于在“可运行”状态下还有另一个同等优先级的线程,该线程现在被调度程序激活并获得 CPU 时间。由于 thread1 从不让出,它一旦激活就会永远运行,从而使 thread0 饥饿。
8. 让我们也通过在打印消息后添加 k_yield() 来使 thread1 让出。
k_yield();C9. 构建应用程序并将其烧录到您的开发套件中。使用串口终端,您现在应该看到以下输出:
*** Booting nRF Connect SDK ***
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1TerminalCPU 上的事件看起来与下面的类似

由于两个线程现在都在打印消息后让出,调度程序在每个线程之间介入,以评估队列中是否有任何可运行的线程。由于两个线程具有相同的优先级,调度程序将始终选择另一个线程作为下一个运行线程,从而导致两个线程在每次运行后交替进行。
这样做的缺点是,如此频繁地让出并因此调用调度程序也会占用 CPU 时间。调度程序在每次调用 k_yield() 时都会使用 CPU 时间来执行内核资源的簿记工作,这反过来会消耗功率。一个架构良好的系统需要设计好您的线程,使调度程序使用最少的 CPU 时间,即设计具有正确优先级且对其他线程相当体谅(让出/睡眠/等待)的线程。
线程睡眠
由于打印是一项非关键任务,我们的线程降低打印频率是可以接受的。因此,更好的选择是让线程睡眠而不是让出。睡眠时,线程被置于“不可运行”状态,处理量非常小。这是通过使用 k_sleep() 或某些派生函数(如 k_msleep())来完成的。
k_sleep() 需要一个 k_timeout_t 类型的参数,可以使用如下宏来构造: K_MSEC(). 为了简单起见,k_sleep() 具有更易于使用的派生函数,如 k_msleep() 和 k_usleep() ,您可以直接传递时间单位。后者将被广泛使用。
10. 让我们尝试一下,将两个线程中的 k_yield() 函数替换为 k_msleep(5),即 5 毫秒的睡眠时间。现在线程应该如下所示:
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);
}
}C11. 构建应用程序并将其烧录到您的开发套件中。使用串口终端,您现在应该看到以下输出:
*** Booting nRF Connect SDK ***
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1
Hello, I am thread0
Hello, I am thread1Terminal虽然输出看起来与线程使用 k_yield() 时完全相同,但这里有一个显著的区别。线程打印频率降低,从而调用调度程序的频率也降低了。请看下面的时间序列图,并观察系统可以切换到低功耗状态的空闲期。

如您所见,当两个线程都睡眠一定时间,并且没有其他可以被调度程序激活的就绪线程时,它会使空闲线程(系统线程之一)激活。
结论:
k_yield() 将线程状态从“运行中”更改为“可运行”,这意味着在下一个重新调度点,刚刚让出的线程仍然是调度程序算法中使线程激活(“运行中”)的候选者。总体结果是,在线程让出后,可运行线程队列中将至少有一个项供调度程序在下一个重新调度点选择。 k_sleep() 将线程状态从“运行中”更改为“不可运行”,直到超时时间已过,然后将其更改为“可运行”。这意味着在超时时间过去之前,该线程不会成为调度程序算法中的候选者。因此,线程睡眠是增加延迟的更好选择,而不是为了让出。此练习的解决方案可以在 GitHub 仓库的 l7/l7_e1_sol 中找到。