Title Image

Blog Logo

🌱 Khái niệm Reentrancy - Reentrant Function / Atomic Variables

🌱 Khái niệm Reentrancy - Reentrant Function / Atomic Variables

    Hầu như mọi hệ thống nhúng đều sử dụng các ngắt - Interrupt; và/hoặc hỗ trợ hoạt động đa nhiệm (multitasking) hoặc đa luồng (multithread). Những loại ứng dụng này có thể mong đợi luồng điều khiển của chương trình sẽ thay đổi ngữ cảnh bất cứ lúc nào.

    Khi ngắt đó xảy ra, thao tác hiện tại sẽ bị tạm dừng và chức năng hoặc tác vụ khác sẽ bắt đầu chạy. Điều gì sẽ xảy ra nếu các hàm và tác vụ đó có sử dụng chung các biến (shared variables)? 

    ➥ Các lỗi bất ngờ chắc chắn sẽ xảy ra nếu một task thay đổi dữ liệu của một task khác. Bằng cách kiểm soát cẩn thận cách chia sẻ dữ liệu, chúng ta có khái niệm hàm “reentrant”, những hàm cho phép nhiều lệnh gọi đồng thời không ảnh hưởng lẫn nhau. Từ “pure” đôi khi được sử dụng thay thế cho “reentrant”.

    👉 Vậy Reentrant là gì?

    Reentrancy trong hệ thống nhúng (embedded system) là khả năng của một hàm hoặc một phần của chương trình có thể được gọi đa luồng mà không gây ra xung đột hoặc lỗi.

    Trong ngữ cảnh của hệ thống nhúng, việc phát triển các hàm reentrant là rất quan trọng vì hệ thống thường phải xử lý nhiều tác vụ cùng một lúc. Khi một hàm là reentrant, nó có thể được gọi từ nhiều luồng thực thi khác nhau mà không cần phải quản lý trạng thái nội bộ của hàm.

    Việc sử dụng các hàm reentrant trong hệ thống nhúng giúp tăng tính linh hoạt và hiệu suất của chương trình, đồng thời giảm thiểu rủi ro xảy ra lỗi khi sử dụng đa luồng.

    Trong hệ thống nhúng, một function phải đáp ứng các nhu cầu sau để được xem là "reentrant":

  1. Nó sử dụng tất cả các shared variables theo cách atomic variable.
  2. Nó không gọi các hàm non-reentrant.
  3. Nó không sử dụng phần cứng theo cách non-atomic.

    👉 Rule 1 - Atomic Variables

    Trong 3 quy tắc nói trên, quy tắc đầu và cuối đều sử dụng từ khóa "atomic", nó xuất phát từ tiếng Hy Lạp - nghĩa là "indivisible" (không thể chia cắt). Trong computer science, "atomic" có nghĩa là một hành động không thể bị gián đoạn. Ví dụ đơn giản là một câu lệnh assembly:

MOV R0, R1

    Nói đơn giản thì câu lệnh ASM trên chỉ có thể dừng lại khi một hành động reset được thực hiện. Cho dù một Interrupt xảy ra, câu lệnh trên vẫn sẽ được thực hiện xong, trước khi chuyển sang các tác vụ của Interrupt.

    Rule 1 ở trên yêu cầu shared variables cần được sử dụng theo các atomic variable. Giả sử 2 functions chia sẻ biến global "foobar". Function A bao gồm đoạn code:

foobar += 1;

    Đoạn code trên là non-reentrant, bởi vì biến foobar được sử dụng non-atomical. Thực tế nó được compile thành 3 câu lệnh assembly (ARM processor):

LDR R0, [foobar] 

ADD R0, 1

STR R0,[foobar]

    Lý do là nó sử dụng 3 câu lệnh để thay đổi giá trị thay vì 1 câu lệnh. Một Interrupt có thể xảy ra giữa các câu lệnh này, chương trình sẽ nhảy sang ISR hoặc task khác, và task đó hoàn toàn có thể sẽ thay đổi biến foobar. Lúc này chương trình sẽ hoạt động không như mong muốn và có thể bị crash.

    ➤ Đây chính là hiện tượng Race Condition

    Rule 1 ở trên mong muốn chúng ta có thể sử dụng các biến local thay thế cho các biến global hoặc dynamic allocation khi có thể.

    👉 Rule 2, 3

    Rule 2 khá dễ hiểu khi một funtion sẽ không được coi là reentrant nếu nó call đến một non-reentrant function.

    Rule 3 là một trường hợp riêng biệt chỉ có trong các hệ nhúng. Các Hardware (peripherals, memory) cũng có thể coi như một variable; Vì bản thân việc access xuống hardware cũng giống như việc access vào một biến global.

    👉 Hãy viết code Reentrant - Viết như thế nào?

    Có những cách nào viết code reentrant? Option nào là tốt nhất để loại bỏ những đoạn code non-reentrant?

    1 - Tránh sử dụng các shared variables

    Các biến global được share giữa các thread/task/sub-routine có thể dễ gây ra các vấn đề kể trên. Vì vậy, trong trường hợp không cần thiết thì hãy tránh sử dụng các biến shared global, thay vào đó là dùng các biến local / dynamic allocation.

    * Mình tạm gọi các biến global sử dụng chung giữa các task/thread là shared global cho ngắn gọn (Vì trong C không có khái niệm này).

    2 - Đảm bảo an toàn khi truy cập shared variables

    Tuy nhiên, sử dụng biến shared global là khá phổ biến trong việc truyền thông tin giữa các task. Vì vậy, khi thực sự cần dùng đến shared global, chúng ta cần có những phương pháp để đảm bảo an toàn.

    Cách tiếp cận đơn giản nhất là disable các ngắt trước khi có một non-atomic statement:

uint8_t i;
void do_something(void)
{
        __irq_disable();
        i += 1;
        __ire_enable();
}

    Tuy nhiên dễ thấy cách này không ổn lắm! Vì hàm do_something() có thể được sử dụng nhiều lần, và việc disable interrupt có thể gây ra việc mất các sự kiện ngắt, gây ra độ trễ của các sự kiện ngắt! gây ra nhiều vấn đề khác nghiêm trọng không kém!

    Vì vậy, một cách nhẹ nhàng hơn trong trường hợp sử dụng chung resource là sử dụng Semaphore hoặc Mutex.

    👉 Recursion - Đệ quy

    Không thể nói về reentrant mà không đề cập đến đệ quy, chủ yếu vì có rất nhiều sự nhầm lẫn giữa hai khái niệm này.

    Một hàm được gọi là đệ quy - recursion nếu nó gọi chính nó. Đó là một cách cổ điển để loại bỏ vòng lặp từ nhiều loại thuật toán. Nếu có đủ không gian stack, đây là một cách viết code hoàn toàn hợp lệ - mặc dù khó debug hơn. 

    Vì một hàm đệ quy gọi chính nó, rõ ràng nó phải là reentrant, để tránh xáo trộn các biến của nó. Vì vậy, tất cả các hàm đệ quy phải là reentrant... nhưng không phải tất cả các hàm reentrant đều là đệ quy.

    👉 Kết luận

    Quan trọng nhất là không tất cả các hàm thư viện C đều có tính reentrancy, vì vậy bạn nên tham khảo tài liệu cho các hàm cụ thể mà bạn đang sử dụng để xác định tính reentrancy của chúng. Nếu một hàm không có tính reentrancy, bạn có thể cần sử dụng các cơ chế đồng bộ hóa như mutex để đảm bảo việc sử dụng an toàn của nó trong môi trường đa luồng.

    Hi vọng rằng thông qua bài viết này, bạn đã hiểu rõ hơn về khái niệm reentrancy trong lập trình C và cách áp dụng nó trong việc viết các hàm an toàn cho đa luồng.

Các bài viết khác về RTOS

Đăng nhận xét

1 Nhận xét