Understanding Synchronization and Mutual Exclusion in RTOS

1. The Concepts of Synchronization and Mutual Exclusion

To understand synchronization and mutual exclusion in one sentence: I wait for you to finish using the bathroom before I use it.

What is synchronization? It is: Hey, I am using the bathroom, please wait.

What is mutual exclusion? It is: Hey, I am using the bathroom, you cannot come in.

Synchronization and mutual exclusion are often discussed together because they are closely related; “mutual exclusion” operations can use “synchronization” to be implemented. I wait for you to finish using the bathroom before I use it. Isn’t this using “synchronization” to achieve “mutual exclusion”?

For another example, in a team activity, colleague A must finish the report before manager B can take it to report to the leadership.

Manager B must wait for colleague A to complete the report, creating a dependency between A and B, which is called synchronization. In team activities, if colleague A is already using the meeting room, manager B also wants to use it; even if manager B is the leader, he must wait. This is called mutual exclusion. Manager B tells colleague A: Please remind me when you finish using the meeting room. This is using “synchronization” to achieve “mutual exclusion”.

void抢厕所(void){ if(有人在用) 我眯一会; 用厕所; 喂,醒醒,有人要用厕所吗;}

Suppose there are two people, A and B, who wake up early to rush for the bathroom. A gets there first and occupies it; B is a bit late, so he takes a nap. When A is done, he wakes up B, and B happily goes to the bathroom.

In this process, A and B are mutually exclusive in accessing the “bathroom,” which is referred to as a critical resource. We used the “sleep-wake” synchronization mechanism to achieve “mutual access” to the “critical resource”.

A resource that can only be used by one person at a time is called a critical resource. For example, if tasks A and B both need to use the serial port to print, the serial port is the critical resource. If A and B use the serial port simultaneously, the printed information will be mixed up and indistinguishable. Therefore, when using the serial port, it should be like this: A finishes, then B uses it; B finishes, then A uses it.

2. Synchronization and Mutual Exclusion Are Not Simple

In bare-metal programs, a global or static variable can be used to achieve mutual exclusion; for example, to mutually exclude using an LCD, the following code can be used:

int LCD_PrintString(int x, int y, char *str){ static int bCanUse = 1; if(bCanUse) { bCanUse = 0; /* Use LCD */ bCanUse = 1; return 0; } return -1; }

However, in RTOS, using the above code to implement mutual exclusion is likely to work, but it cannot ensure absolute safety.

Consider the following scenario: there are two tasks, A and B, both wanting to call LCD_PrintString. When task A executes line 4, it finds bCanUse is 1 and can enter the if statement. Before it executes line 6, it gets switched out; then task B also calls LCD_PrintString. When task B executes line 4, it also finds bCanUse is 1, allowing it to enter the if statement and use the LCD. In this case, using a static variable does not achieve mutual exclusion.

The issue here is that lines 4 and 6 were interrupted. An improvement would be to decrement bCanUse at the function entry. Can this ensure absolute mutual exclusion?

int LCD_PrintString(int x, int y, char *str){ static int bCanUse = 1; bCanUse--; if(bCanUse == 0) { /* Use LCD */ bCanUse++; return 0; } else { bCanUse++; return -1; }}

The assembly representation of line 4 is as follows:

04.1 LDR R0, [bCanUse] // Load bCanUse value into register R004.2 DEC R0, #1 // Decrement R004.3 STR R0, [bCanUse] // Write R0 back to bCanUse

Assume a scenario where two tasks A and B both want to call LCD_PrintString. When task A executes line 04.1, it reads bCanUse as 1, stores it in register R0, and gets switched out; then task B also calls LCD_PrintString. When task B executes line 4, it finds bCanUse as 1 and decrements it to 0, executing line 5 and finding the condition satisfied, allowing it to enter the if statement to use the LCD. Task B then gets switched out; now task A continues running line 04.2, with R0 as 1, and when it runs line 04.3, it sets bCanUse to 0, allowing it to successfully enter the if statement. In this case, both task A and task B can use the LCD.

The reason this method cannot guarantee absolute safety is that it can be interrupted during the check. If this process can be ensured not to be interrupted, it would work: by disabling interrupts.

The code improvement for example 1 is as follows: disable interrupts before lines 5-7.

int LCD_PrintString(int x, int y, char *str){ static int bCanUse = 1; disable_irq(); // Disable interrupts if(bCanUse) { bCanUse = 0; enable_irq(); // Enable interrupts /* Use LCD */ bCanUse = 1; return 0; } enable_irq(); // Enable interrupts return -1;}

The code improvement for example 2 is as follows: disable interrupts before line 5.

int LCD_PrintString(int x, int y, char *str){ static int bCanUse = 1; disable_irq(); bCanUse--; enable_irq(); if(bCanUse == 0) { /* Use LCD */ bCanUse++; return 0; } else { disable_irq(); bCanUse++; enable_irq(); return -1; }}

Disabling interrupts is not foolproof: suppose now tasks A and B are executing the following function. After A prints for 1ms, B is scheduled, but B just performs the check and continues failing; after 1ms, A is scheduled to continue printing. After A finishes printing, 1ms later, it’s B’s turn again, and B continues checking and fails. This results in B occupying CPU resources, similar to the synchronization example. The solution is: if task B finds that A is already printing, it should be set to a blocked state, allowing A to continue printing. Once A finishes printing, B can be awakened to print normally.

void CalTask(void *params) // Timing function { uint32_t i = 0; time = system_get_ns(); // Time at this point for(i=0;i<10000000;i++) { sum += i; } Cal_end = 1; // Set calculation flag time = system_get_ns() - time; // Calculate time taken for the loop vTaskDelete(NULL); } void LcdPrintTask(void *params) { int len; while(1) { /* Print information */ LCD_PrintString(0,0,"waiting"); vTaskDelay(2000); // Block this task until the above task completes to prevent CPU resource occupation while(Cal_end == 0); // Wait for the above calculation to finish if(flag) { flag = 0; LCD_ClearLine(0,0); len = LCD_PrintString(0,0,"Sum:"); len += LCD_PrintHex(len,0,sum,1); len += LCD_PrintSignedVal(len,2,time/1000000); flag = 1; } vTaskDelete(NULL); }} /* Creating tasks in FreeRTOS.c */ xTaskCreate(CalTask,"taskA",128,NULL,osPriorityNormal,NULL); xTaskCreate(LcdPrintTask,"taskB",128,&Task2,osPriorityNormal,NULL); 

Synchronization example (task B is not blocked)

Understanding Synchronization and Mutual Exclusion in RTOS

Understanding Synchronization and Mutual Exclusion in RTOS

Task A continuously times, and after 1s of execution, task B executes, while task B calls a blocking function, so it continues to allow task A to execute. After task A finishes, it jumps to task B, which successfully prints the timing value. (Here, the counting value mainly comes from task A, so if task A finishes, the timing will stop. Therefore, the blocking delay in B (just a 2s delay) does not affect the timing result, ensuring that we can accurately obtain the timing value.)

3. Comparison of Various Methods

Kernel methods that can achieve synchronization and mutual exclusion include: task notifications, queues, event groups, semaphores, and mutexes.

They all have similar operational methods: acquire/release, block/wake, timeout. For example:

Task A acquires a resource, and after use, task A releases the resource.

If task A cannot acquire the resource, it blocks; task B releases the resource and wakes task A.

If task A cannot acquire the resource, it blocks and sets an alarm; A either times out or is awakened by task B releasing the resource.

To distinguish them, consider:

Can information be passed? Or can only states be conveyed?

Is it for everyone (all tasks can use it)? Or just for you (only specified tasks can use it)?

I produce, you consume?

I lock, and only I can unlock.

Understanding Synchronization and Mutual Exclusion in RTOS

The graphical comparison is as follows:

Queue:

Can hold arbitrary data, can hold multiple data.

Tasks and ISR can both enqueue and dequeue data.

Event Group:

An event is represented by a bit, with 1 indicating the event has occurred and 0 indicating it has not.

Can represent events or combinations of events, but cannot pass data.

Has a broadcast effect: when an event or combination of events occurs, multiple tasks waiting for it will be awakened.

Semaphore:

The core is the “count value”.

When a task or ISR releases a semaphore, the count value increases by 1.

When a task or ISR acquires a semaphore, the count value decreases by 1.

Task Notification:

The core is the value in the task’s TCB.

It will be overwritten.

Who receives the notification? The receiving task must be specified.

Only the receiving task can retrieve that notification.

Mutex:

Only has values of 0 or 1.

Who acquires the mutex must also release the same mutex.

Understanding Synchronization and Mutual Exclusion in RTOS

Understanding Synchronization and Mutual Exclusion in RTOS

Understanding Synchronization and Mutual Exclusion in RTOS

Understanding Synchronization and Mutual Exclusion in RTOS

Understanding Synchronization and Mutual Exclusion in RTOS

Leave a Comment