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ết | Phổ biến | Ghi chú |
|---|---|---|
| while (1) | Nhiều nhất | Dễ đọc nhất, ai cũng hiểu ngay |
| for (;;) | Nhiều | Bỏ 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ếm | Khô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ân | Ví dụ | Cách sửa |
|---|---|---|
| Quên cập nhật biến | while (i < 10) { printf(...); } | Thêm i++, hoặc chuyển sang for |
| Điều kiện không bao giờ đạt tới | while (i != 10) i += 3; | Dùng < thay cho != |
| So sánh số thực bằng dấu bằng | while (f != 1.0) f += 0.1; | Đếm bằng số nguyên |
| continue trước phần tăng biến | while (...) { if (x) continue; i++; } | Chuyển sang for |
| Đếm ngược bằng kiểu không dấu | for (size_t i = n-1; i >= 0; --i) | Dùng i-- > 0 |
| scanf thất bại không dọn bộ đệm | while (scanf("%d", &n) != 1) { ... } | Dọn bộ đệm, hoặc dùng fgets |
| break chỉ thoát switch | while (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.
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.
terminalCtrl + C# Nếu không dừng đượcpkill chuong-trinhGắ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.
terminalgdb -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 iThê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); /* ... */ }Đặ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; } /* ... */ }
while (1) {
xu_ly();
}/* 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ử
- 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,chaovàthoat. - 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. - 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.
- 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.
- Đặt giới hạn bộ nhớ bằng
ulimit -vrồ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 (;;)và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 -prồibtvàprintđể 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.