Đọc Đầu Vào Chuẩn Theo Chế Độ Không Chặn Trên Windows - nói dối e blog

Đọc Đầu Vào Chuẩn Theo Chế Độ Không Chặn Trên Windows

Trong quá trình phát triển và gỡ lỗi một trò chơi client, mình gặp phải vấn đề nhỏ cần xử lý lệnh đầu vào từ bảng điều khiển. Thực ra yêu cầu này đã tồn tại từ lâu, trước đây mình từng tự viết một bảng điều khiển riêng nên dễ kiểm soát hơn. Mặc dù đã nhiều lần sử dụng bảng điều khiển chuẩn của Windows, nhưng vấn đề xử lý đầu vào vẫn chưa được giải quyết triệt để. Lần này quyết định tìm giải pháp tối ưu hơn.

Các hàm C truyền thống như scanf hay gets đều hoạt động theo cơ chế chặn (blocking) - chương trình sẽ bị treo tại chỗ cho đến khi có đầu vào. Ban đầu mình nghĩ đơn giản là chuyển file đầu vào chuẩn sang chế độ không chặn (non-blocking). Tuy nhiên, khác với hệ thống Unix/Linux có sẵn fcntl hay ioctl, Windows không hỗ trợ các công cụ này để thay đổi chế độ của stdin. Mặc dù có thể tồn tại các API Windows đặc thù, nhưng mình không có thời gian nghiên cứu sâu.

Phương pháp phổ biến nhưng kém “thẩm mỹ” là sử dụng _kbhit để kiểm tra phím nhấn. Mình quyết định chọn cách tạo thêm một luồng (thread) riêng xử lý đầu vào. Dù giải pháp này tiêu tốn tài nguyên hơn, nhưng với mục đích gỡ lỗi thì vẫn chấp nhận được.

Dưới đây là đoạn mã đã kiểm chứng hiệu quả:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include "windows.h"
#include "process.h"
#include "stdio.h"
#define BUFFER_MAX 1024
char g_nbstdin_buffer[2][BUFFER_MAX];
HANDLE g_input[2];
HANDLE g_process[2];

DWORD WINAPI console_input(LPVOID lpParameter)
{
  for (;;) {
    int i;
    for (i=0; i<2; i++) {
      fgets(g_nbstdin_buffer[i], BUFFER_MAX, stdin);
      SetEvent(g_input[i]);
      WaitForSingleObject(g_process[i], INFINITE);
    }
  }
  return 0;
}

void create_nbstdin()
{
  int i;
  DWORD tid;
  CreateThread(NULL, 1024, &console_input, 0, 0, &tid);
  for (i=0; i<2; i++) {
    g_input[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
    g_process[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
    g_nbstdin_buffer[i][0] = '\0';
  }
}

const char* nbstdin()
{
  DWORD n = WaitForMultipleObjects(2, g_input, FALSE, 0);
  if (n == WAIT_OBJECT_0 || n == WAIT_OBJECT_0+1) {
    n = n - WAIT_OBJECT_0;
    SetEvent(g_process[n]);
    return g_nbstdin_buffer[n];
  }
  else {
    return NULL;
  }
}

void main()
{
  create_nbstdin();
  for (;;) {
    const char *line = nbstdin();
    if (line) {
      printf(">%s", line);
    }
    else {
      Sleep(0);
    }
  }
}

Chương trình này sử dụng một luồng phụ chuyên trách đọc stdin, đồng thời triển khai hàm nbstdin() hoạt động tương tự gets() nhưng không chặn. Khi không có dữ liệu đầu vào mới, hàm sẽ trả về con trỏ NULL thay vì làm treo chương trình.

Điểm đặc biệt là cơ chế hai bộ đệm (double buffering) được áp dụng - khi một luồng đang ghi dữ liệu vào buffer này, luồng chính có thể an toàn xử lý buffer kia. Điều này đảm bảo tính toàn vẹn dữ liệu giữa các lần gọi hàm.

Mặc dù chương trình không giải phóng các sự kiện (event) đã tạo hay đóng luồng phụ khi kết thúc, nhưng điều này không gây ảnh hưởng nghiêm trọng. Hệ điều hành sẽ tự động dọn dẹp tài nguyên khi tiến trình kết thúc, giúp mã nguồn gọn gàng hơn mà vẫn đảm bảo hiệu năng chấp nhận được trong môi trường phát triển.

Lưu ý: Đây là giải pháp tối ưu cho mục đích gỡ lỗi, nhưng không khuyến khích sử dụng nguyên样 trong môi trường sản phẩm do tiêu tốn tài nguyên hệ thống. Với ứng dụng thương mại, nên nghiên cứu kỹ các API Windows chuyên dụng như ReadConsoleInput hay sử dụng thư viện đa nền tảng như libuv để xử lý đầu vào không chặn.

0%