使用HID API实现设备非阻塞读取的技术咨询
解决方案
1. 用I/O完成端口(IOCP)管理多设备异步读取
IOCP是Windows下高效处理多设备I/O请求的机制,无需为每个设备单独创建线程,仅用少量工作线程即可处理所有设备的输入通知,完美适配你的需求。
实现步骤
- 创建完成端口,将所有已打开的HID设备句柄关联到该端口。
- 为每个设备发起异步
ReadFile请求(借助OVERLAPPED结构),请求会挂起直至有输入或超时。 - 启动少量工作线程(数量建议与CPU核心数相当),通过
GetQueuedCompletionStatus等待完成通知,收到数据后处理并重新发起异步读,保持持续监听。
代码片段
// 创建完成端口 HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); if (hIOCP == NULL) { /* 错误处理逻辑 */ } // 关联所有HID设备到完成端口并发起首次异步读 for (size_t i = 0; i < deviceHandles.size(); ++i) { HANDLE hDevice = deviceHandles[i]; CreateIoCompletionPort(hDevice, hIOCP, (ULONG_PTR)hDevice, 0); OVERLAPPED* pOv = new OVERLAPPED{0}; pOv->hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); BYTE* pBuffer = new BYTE[REPORT_SIZE]; // REPORT_SIZE为设备报告长度 ReadFile(hDevice, pBuffer, REPORT_SIZE, NULL, pOv); } // 工作线程函数 DWORD WINAPI IOCPWorker(LPVOID param) { HANDLE hIOCP = (HANDLE)param; DWORD bytesRead; ULONG_PTR deviceKey; OVERLAPPED* pOv; while (GetQueuedCompletionStatus(hIOCP, &bytesRead, &deviceKey, &pOv, INFINITE)) { HANDLE hDevice = (HANDLE)deviceKey; if (bytesRead > 0) { // 将读取到的数据缓存到全局设备状态结构体中 UpdateDeviceCache(hDevice, (BYTE*)pOv->hEvent, bytesRead); // 此处可自行调整缓存逻辑 } // 重新发起异步读,保持监听 ZeroMemory(pOv, sizeof(OVERLAPPED)); pOv->hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); BYTE* pBuffer = new BYTE[REPORT_SIZE]; ReadFile(hDevice, pBuffer, REPORT_SIZE, NULL, pOv); } return 0; } // 启动工作线程(示例为2个,可根据CPU核心数调整) CreateThread(NULL, 0, IOCPWorker, hIOCP, 0, NULL); CreateThread(NULL, 0, IOCPWorker, hIOCP, 0, NULL);
2. 非阻塞模式+定时轮询
若不需要实时响应,仅需定期获取设备更新,可将设备设置为非阻塞模式,通过定时器定时轮询所有设备的ReadFile调用。
实现步骤
- 打开设备时指定
FILE_FLAG_OVERLAPPED,或通过SetCommTimeouts设置读取超时,让ReadFile无数据时立即返回。 - 用定时器定期触发轮询函数,遍历所有设备读取数据并缓存。
代码片段
// 打开设备后设置非阻塞读取超时 COMMTIMEOUTS timeouts = {0}; timeouts.ReadIntervalTimeout = MAXDWORD; timeouts.ReadTotalTimeoutConstant = 0; timeouts.ReadTotalTimeoutMultiplier = 0; SetCommTimeouts(hDevice, &timeouts); // 全局缓存设备最新状态 unordered_map<HANDLE, vector<BYTE>> deviceCache; // 轮询函数,由定时器触发 VOID CALLBACK PollDevices(HWND hwnd, UINT msg, UINT_PTR id, DWORD time) { for (HANDLE hDevice : deviceHandles) { BYTE buffer[REPORT_SIZE]; DWORD bytesRead; BOOL result = ReadFile(hDevice, buffer, REPORT_SIZE, &bytesRead, NULL); if (result && bytesRead > 0) { // 更新缓存 deviceCache[hDevice] = vector<BYTE>(buffer, buffer + bytesRead); } } } // 设置定时器,每500ms轮询一次(可调整间隔) SetTimer(NULL, 1, 500, PollDevices);
3. 重叠IO+多事件等待(适合设备数量少的场景)
若设备数量不超过64个(MAXIMUM_WAIT_OBJECTS上限),可为每个设备的OVERLAPPED结构绑定事件,通过WaitForMultipleObjects等待事件触发,无需多线程即可处理异步通知。
代码片段
// 初始化每个设备的异步读事件 vector<OVERLAPPED> ovList; vector<HANDLE> eventList; for (HANDLE hDevice : deviceHandles) { OVERLAPPED ov = {0}; ov.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); ovList.push_back(ov); eventList.push_back(ov.hEvent); BYTE buffer[REPORT_SIZE]; ReadFile(hDevice, buffer, REPORT_SIZE, NULL, &ov); } // 主线程循环处理 while (true) { DWORD waitResult = WaitForMultipleObjects(eventList.size(), eventList.data(), FALSE, 100); // 100ms超时 if (waitResult >= WAIT_OBJECT_0 && waitResult < WAIT_OBJECT_0 + eventList.size()) { size_t idx = waitResult - WAIT_OBJECT_0; HANDLE hDevice = deviceHandles[idx]; OVERLAPPED* pOv = &ovList[idx]; DWORD bytesRead; GetOverlappedResult(hDevice, pOv, &bytesRead, FALSE); if (bytesRead > 0) { // 更新设备缓存 UpdateDeviceCache(hDevice, (BYTE*)pOv->hEvent, bytesRead); } // 重置事件并重新发起异步读 ResetEvent(pOv->hEvent); BYTE buffer[REPORT_SIZE]; ReadFile(hDevice, buffer, REPORT_SIZE, NULL, pOv); } // 此处可随时读取deviceCache中的设备最新数据 }
核心注意事项
- 所有方案都需要维护设备输入的缓存,这样你可以在任意时间直接读取缓存中的最新信息,无需等待实时输入。
- 异步读取完成后必须重新发起读请求,否则后续设备输入不会被捕获。
- 使用异步IO时,打开HID设备必须指定
FILE_FLAG_OVERLAPPED参数。
内容的提问来源于stack exchange,提问作者Zachary Peterson
相关产品推荐
相关产品推荐

