Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 6.613 phút đọc

Vòng lặp vô hạn

Sau bài này bạn sẽ làm được

  • Viết vòng lặp vô hạn bằng ba cách và biết cách nào phổ biến nhất
  • Kể ba ứng dụng hợp lệ của vòng lặp vô hạn
  • Chẩn đoán nguyên nhân khi chương trình treo
  • Dùng công cụ để tìm chỗ vòng lặp bị kẹt

Vòng lặp không bao giờ tự dừng nghe như một lỗi, nhưng nó là cấu trúc nền tảng của server, của giao diện đồ họa và của mọi firmware nhúng. Bài này phân biệt lúc nào nó là thiết kế đúng, lúc nào là lỗi, và cách tìm ra chỗ kẹt khi chương trình treo.

#Ba cách viết

while (1) {
    /* thân */
}

for (;;) {
    /* thân */
}

do {
    /* thân */
} while (1);
Cách viếtPhổ biếnGhi chú
while (1)Nhiều nhấtDễ đọc nhất, ai cũng hiểu ngay
for (;;)NhiềuBỏ trống cả ba phần. Không sinh cảnh báo với mọi trình biên dịch
do while (1)Rất hiếmKhông có lợi thế gì so với hai cách trên

#Ba ứng dụng hợp lệ

Ứng dụng 1: vòng lặp sự kiện

Server, chương trình có giao diện và trò chơi đều chạy theo mô hình này: chờ một sự kiện, xử lý, rồi chờ tiếp. Chương trình chỉ dừng khi người dùng yêu cầu hoặc khi có tín hiệu tắt.

while (1) {
    int fd = accept(sock, NULL, NULL);   /* chờ kết nối mới, học ở Chương 46 */

    if (fd < 0) {
        if (errno == EINTR) {
            continue;                    /* bị tín hiệu ngắt, thử lại */
        }

        break;                           /* lỗi thật, thoát */
    }

    xu_ly_ket_noi(fd);
    close(fd);
}

Ứng dụng 2: vòng lặp chính của firmware

Vi điều khiển không có hệ điều hành để trả quyền về. Nếu hàm main kết thúc thì con chip không còn gì để chạy, nên vòng lặp vô hạn là bắt buộc. Chi tiết ở Phần 18.

int main(void)
{
    khoi_tao_phan_cung();

    for (;;) {
        doc_cam_bien();
        xu_ly();
        cap_nhat_dau_ra();
    }

    /* Không bao giờ tới đây. */
}

Ứng dụng 3: menu và vòng lặp nhập liệu

Khi điều kiện dừng chỉ xác định được ở giữa thân, viết vòng lặp vô hạn rồi break lại rõ ràng hơn là cố nhồi điều kiện lên đầu.

while (1) {
    char dong[128];

    printf("> ");

    if (fgets(dong, sizeof dong, stdin) == NULL) {
        break;                       /* hết dữ liệu vào */
    }

    dong[strcspn(dong, "\n")] = '\0';

    if (strcmp(dong, "thoat") == 0) {
        break;                       /* người dùng yêu cầu dừng */
    }

    thuc_thi(dong);
}

#Khi vô hạn ngoài ý muốn

Gom lại đầy đủ các nguyên nhân đã gặp rải rác trong cả chương, cộng thêm hai nguyên nhân mới.

Nguyên nhânVí dụCách sửa
Quên cập nhật biếnwhile (i < 10) { printf(...); }Thêm i++, hoặc chuyển sang for
Điều kiện không bao giờ đạt tớiwhile (i != 10) i += 3;Dùng < thay cho !=
So sánh số thực bằng dấu bằngwhile (f != 1.0) f += 0.1;Đếm bằng số nguyên
continue trước phần tăng biếnwhile (...) { if (x) continue; i++; }Chuyển sang for
Đếm ngược bằng kiểu không dấufor (size_t i = n-1; i >= 0; --i)Dùng i-- > 0
scanf thất bại không dọn bộ đệmwhile (scanf("%d", &n) != 1) { ... }Dọn bộ đệm, hoặc dùng fgets
break chỉ thoát switchwhile (1) { switch (x) { case 0: break; } }Dùng biến cờ

#Quy trình chẩn đoán

Khi chương trình treo, đừng đoán mò. Có một quy trình bốn bước cho ra câu trả lời trong vài phút.

  1. Dừng chương trình lại

    Bấm tổ hợp phím dừng trong cửa sổ đang chạy. Nếu nó không phản hồi thì tìm và kết thúc tiến trình từ cửa sổ khác.

    terminal
    Ctrl + C
    # Nếu không dừng được
    pkill chuong-trinh
  2. Gắn trình gỡ lỗi vào tiến trình đang treo

    Cách này cho biết chính xác dòng nào đang chạy và biến điều khiển đang mang giá trị gì. Đây thường là đủ để tìm ra nguyên nhân.

    terminal
    gdb -p $(pidof chuong-trinh)
    (gdb) bt
    #0  main () at bai.c:14
    (gdb) print i
    $1 = 0
    (gdb) print n
    $2 = 10
    # i mãi bằng 0 trong khi n bằng 10, vậy là quên tăng i
  3. Thêm dòng in tạm nếu chưa rõ

    Nếu không dùng được trình gỡ lỗi, in giá trị biến điều khiển ở đầu mỗi lượt. Nhớ ghi ra luồng lỗi chuẩn để không bị mất do bộ đệm, như đã học ở Bài 3.1.

    while (i < n) {
        fprintf(stderr, "[gỡ lỗi] i = %d, n = %d\n", i, n);
        /* ... */
    }
  4. Đặt bộ đếm an toàn khi thử nghiệm

    Trong lúc đang gỡ lỗi, thêm một giới hạn cứng để vòng lặp tự dừng thay vì treo máy. Nhớ xóa nó đi khi đã sửa xong.

    long an_toan = 0;
    
    while (dieu_kien) {
        if (++an_toan > 10000000L) {
            fprintf(stderr, "Nghi ngờ vòng lặp vô hạn\n");
            break;
        }
    
        /* ... */
    }
Không rõ thoát bằng cách nào
while (1) {
    xu_ly();
}
Ghi rõ lối thoát
/* Thoát khi người dùng gõ "thoat" hoặc khi hết dữ liệu vào. */
while (1) {
    if (!doc_lenh(dong, sizeof dong)) {
        break;
    }

    if (strcmp(dong, "thoat") == 0) {
        break;
    }

    xu_ly(dong);
}

Tự làm thử

  1. Viết chương trình đọc lệnh từ bàn phím trong vòng lặp vô hạn, hỗ trợ các lệnh giup, chaothoat.
  2. Viết một vòng lặp vô hạn ngoài ý muốn, chạy nó, rồi dùng gdb -p để tìm ra dòng đang kẹt.
  3. Tái hiện đủ bảy nguyên nhân trong bảng, mỗi cái một chương trình nhỏ, rồi sửa từng cái.
  4. Thêm bộ đếm an toàn vào một vòng lặp, kiểm chứng nó dừng đúng khi vượt ngưỡng.
  5. Đặt giới hạn bộ nhớ bằng ulimit -v rồi chạy vòng lặp có cấp phát không giải phóng, quan sát hệ thống dừng chương trình.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Ba cách viết là while (1), for (;;) do while (1). Hai cách đầu phổ biến như nhau.
  • Ba ứng dụng hợp lệ là vòng lặp sự kiện, vòng lặp chính của firmware, và menu.
  • Vòng lặp vô hạn cố ý phải có lối ra rõ ràng và nên ghi chú thích nói rõ lối đó.
  • Có bảy nguyên nhân khiến vòng lặp vô hạn ngoài ý muốn, xem bảng tổng kết.
  • Khi treo, dùng gdb -p rồi btprint để tìm dòng kẹt và giá trị biến điều khiển.
  • Thêm bộ đếm an toàn trong lúc gỡ lỗi, và nhớ xóa nó khi đã sửa xong.