在本练习中,我们将创建一个包含两个线程运行并访问相同代码段的应用程序。当只有一个线程访问临界区时,逻辑看起来非常完美,但当两个不同的线程尝试同时访问该代码段时,就会发生意外情况(竞态条件)。我们将了解如何利用互斥量来同步这两个线程。
我们在此创建的线程将具有相同的优先级 (4),并且它们将启用为期 10 毫秒的时间片轮转。这意味着每个线程将被给予 10 毫秒的时间来完成其任务,然后它会被调度程序强制抢占,以允许其他同优先级的线程运行。
1. 在本课程的 GitHub 仓库中,打开此练习的基础代码,位于 l8/l8_e2。
2. 在应用程序中启用多线程。这可以通过在 prj.conf 中添加以下内容来完成:
CONFIG_MULTITHREADING=yKconfig此配置在所有 nRF Connect SDK 应用程序中默认为 yes,并非严格需要手动启用。
3. 将 thread0 和 thread1 的优先级设置为相等。
#define THREAD0_PRIORITY 4
#define THREAD1_PRIORITY 4C4. 为这两个线程创建函数。这些函数应在 while 循环中运行函数 shared_code_section()。shared_code_section() 目前应在 thread1 中被注释掉,如下所示。
void thread0(void)
{
printk("Thread 0 started\n");
while (1) {
shared_code_section();
}
}
void thread1(void)
{
printk("Thread 1 started\n");
while (1) {
//shared_code_section();
}
}C5. 定义两个计数器变量 increment_count 和 decrement_count,以及一个常量宏 COMBINED_TOTAL ,将其组合总数设置为一个任意值,例如 40。
#define COMBINED_TOTAL 40
int32_t increment_count = 0;
int32_t decrement_count = COMBINED_TOTAL; C6. 在函数 shared_code_section() 中,添加以下逻辑:
increment_count 递增 increment_count 如果达到 COMBINED_TOTAL 的最大值,则重置为 0decrement_count 递减decrement_count 如果达到最小值 1,则重置为 COMBINED_TOTAL
uint8_t race_condition = 0;
int32_t increment_count_copy = 0;
int32_t decrement_count_copy = 0;
increment_count += 1;
increment_count = increment_count % COMBINED_TOTAL;
decrement_count -= 1;
if (decrement_count == 0)
{
decrement_count = COMBINED_TOTAL;
}C如果此逻辑实现正确,increment_count + decrement_count = COMBINED_TOTAL 应始终为 true。如果在运行过程中的任何时刻打破了这一硬性规则,则说明代码中存在缺陷(竞态条件)。
7. 在 shared_code_section() 中添加一个 if 语句,仅当计数器的组合值不等于 COMBINED_TOTAL 时才打印它们的值。
if (increment_count + decrement_count != COMBINED_TOTAL) {
race_condition = 1;
}
if( race_condition ){
printk("Race condition happend!\n");
printk("Increment_count (%d) + Decrement_count (%d) = %d \n", increment_count,
decrement_count, (increment_count + decrement_count));
k_msleep(400 + sys_rand32_get() % 10);
}C8. 构建应用程序并将其烧录到您的开发套件中。使用串口终端,您应该看到以下输出:
这两个线程被赋予了 5000 毫秒的启动调度延迟,这仅影响线程第一次进入就绪状态。添加此延迟的原因是为了给您足够的时间连接到串口终端,以便您可以观察日志输出。
*** Booting nRF Connect SDK ***
Thread 0 started
Thread 1 startedTerminal我们确认,当只有 thread0 访问 shared_code_section() 时,我们的加减计数器逻辑运行正常(按顺序),因为 printk() 检查没有打印任何内容。
9. 现在通过取消注释该行,让 thread1 访问 shared_code_section(),使其如下所示。
void thread1(void)
{
printk("Thread 1 started\n");
while (1) {
shared_code_section();
}
}C10. 再次构建应用程序并将其烧录到您的开发套件中。您应该看到以下输出:
*** Booting nRF Connect SDK ***
Thread 0 started
Thread 1 started
Race condition happend!
Increment_count (6) + Decrement_count (35) = 41
Race condition happend!
Increment_count (7) + Decrement_count (34) = 41
Race condition happend!
Increment_count (8) + Decrement_count (33) = 41
Race condition happend!
Increment_count (0) + Decrement_count (1) = 1
Race condition happend!
Increment_count (10) + Decrement_count (31) = 41
Race condition happend!
Increment_count (11) + Decrement_count (30) = 41
Race condition happend!
Increment_count (12) + Decrement_count (29) = 41
Race condition happend!
Increment_count (13) + Decrement_count (28) = 41
Race condition happend!
Increment_count (14) + Decrement_count (27) = 41
Race condition happend!
Increment_count (15) + Decrement_count (26) = 41
Race condition happend!
Increment_count (16) + Decrement_count (25) = 41
Race condition happend!
Increment_count (17) + Decrement_count (24) = 41 Terminal一旦 thread1 开始访问共享代码段,我们的工作逻辑就失败了。
这是一个典型的例子,说明需要保护共享代码段,以确保一次只有一个线程可以访问共享代码段(临界区)。我们代码中的根本缺陷是,我们假设对两个计数器(increment_count, decrement_count)的操作将一次性执行而不会中断。当情况并非如此时,因为任何线程都可以随时访问和更新这些共享内存变量,那么对值的检查就会失败。这就是我们可以使用互斥量来序列化多个线程对该共享代码的访问的地方。通过使用互斥量,我们可以强制 shared_code_section() 在不受其他线程中断的情况下运行。
11. 首先通过如下定义互斥量来添加一个互斥量。
K_MUTEX_DEFINE(test_mutex);C12.1 在执行计数器逻辑之前锁定互斥量。
k_mutex_lock(&test_mutex, K_FOREVER);C12.2 当发生竞态条件时复制计数器的值。我们需要在临界区内进行备份,因为它们可能会在打印之前(就在我们解锁互斥量之后)被更改。
if (increment_count + decrement_count != COMBINED_TOTAL) {
race_condition = 1;
increment_count_copy = increment_count;
decrement_count_copy = decrement_count;
}C12.3 在使用这些函数检查计数器总和的 if 语句之后立即解锁。检查值的过程仍需要受互斥量保护,否则计数器的值可能会被另一个任务更改。
k_mutex_unlock(&test_mutex);C12.4 在竞态条件错误消息中使用计数器的副本。
if( race_condition ){
printk("Race condition happend!\n");
printk("Increment_count (%d) + Decrement_count (%d) = %d \n", increment_count_copy,
decrement_count_copy, (increment_count_copy + decrement_count_copy));
k_msleep(400 + sys_rand32_get() % 10);
}C13. 构建应用程序并将其烧录到您的开发套件中。您应该看到与步骤 8 相同的输出,即不打印计数器的值。
此练习的解决方案可以在 GitHub 仓库中找到, l8/l8_e2_sol.