Linux 实现 Windows 的 Event 事件机制

前言

Linux 中没有 Windows 系统中的 CreateEvent()WaitEvent()SetEvent()ResetEvent() 等函数,本文将介绍如何使用 pevents 替代 Linux 缺失的函数。

pevents 介绍

pevents 的简介

pevents 是一个跨平台的轻量级 C++ 库,旨在为 POSIX 系统提供 WIN32 事件的实现。pevents 提供了 Windows 平台手动和自动重置事件的大部分功能,最显著的是支持同时等待多个事件(WaitForMultipleObjects),而且支持 Windows、FreeBSD、Linux、macOS、iOS、Android 等平台。

pevents 的 API

API 函数

pevents 的 API 是根据 Windows 的 CreateEvent()WaitEvent()WaitForMultipleObjects() 函数编写的,熟悉 WIN32 事件的开发人员应该可以将代码库切换到 pevents API。虚假唤醒是 Linux 下系统编程的正常部分,也是来自 Windows 世界的开发人员的常见陷阱,pevents 可以保证不存在虚假唤醒和等待返回的数据的正确性,其提供了如下的 API:

1
2
3
4
5
6
7
8
9
10
int SetEvent(neosmart_event_t event);
int ResetEvent(neosmart_event_t event);
int PulseEvent(neosmart_event_t event);

int DestroyEvent(neosmart_event_t event);
neosmart_event_t CreateEvent(bool manualReset, bool initialState);

int WaitForEvent(neosmart_event_t event, uint64_t milliseconds);
int WaitForMultipleEvents(neosmart_event_t *events, int count, bool waitAll, uint64_t milliseconds);
int WaitForMultipleEvents(neosmart_event_t *events, int count, bool waitAll, uint64_t milliseconds, int &index);

事件状态的类型

  • CreateEvent() 函数
1
2
3
4
5
6
7
neosmart_event_t CreateEvent(
// true:表示手动,在 WaitEvent 后需要手动调用 ResetEvent 清除事件信号。false:表示自动,在 WaitEvent 后,系统会自动清除事件信号
bool manualReset,

// 初始状态,false 为无信号,true 为有信号
bool initialState
);
  • WaitForEvent() 函数
1
2
3
4
5
6
int WaitForEvent(
// 句柄对象
neosmart_event_t event,
// 等待的时间(毫秒)
uint64_t milliseconds
);
  • 事件状态的类型
    • WAIT_TIMEOUT:等待超时
    • WAIT_OBJECT_0:句柄对象处于有信号状态
    • WAIT_FAILED:出现错误,可通过 GetLastError() 函数得到错误码
    • WAIT_ABANDONED:说明句柄代表的对象是个互斥对象,并且正在被其它线程占用

注意

在 Linux 平台,pevents 的事件状态只支持使用 WAIT_TIMEOUT,且有信号的时候 WaitEvent() 函数的返回值是 0,而在 Windows 平台则支持上述四种事件状态

pevents 的项目结构

  • 核心代码在 src/ 目录
  • 单元测试代码(通过 Meson 构建)在 test/ 目录
  • examples/ 目录中可以找到演示 pevents 用法的跨平台应用示例程序

pevents 的编译构建

pevents 使用的构建工具是 Meson,目前这仅用于支持 pevents 核心代码及其单元测试的自动化构建 / 测试。值得一提的是,开发人员不需要担心构建工具的差异性,pevents 是特意基于 C/C++ 标准编写的,避免了复杂的配置或依赖于平台的构建指令的需要。

pevents 的编译参数

通过编译参数 -DWFMO-DPULSE,可以在编译时让 pevents 启用不同的功能:

  • WFMO:启用 WFMO 功能,如果需要使用 WaitForMultipleEvents() 函数,建议仅使用 WFMO 进行编译,因为它会为所有事件对象增加开销(较小)。
  • PULSE:启用 PulseEvent 功能,PulseEvent() 在 Windows 平台从根本上被破坏了,一般不应该被使用,当你调用它时,它几乎永远不会做你认为你正在做的事情。pevents 包含这个函数只是为了让现有的(有缺陷的)代码从 WIN32 移植到 Unix/Linux 平台更容易,并且这个函数默认没有编译到 pevents 中。

Meson 指定编译参数

在 Meson 中,可以通过 meson_options.txt 配置文件指定编译参数,让 pevents 启用不同的功能

1
2
3
4
option('wfmo', type: 'boolean', value: true,
description: 'Enable WFMO events')
option('pulse', type: 'boolean', value: false,
description: 'Enable PulseEvent() function')

CMake 指定编译参数

在 CMake 中,可以通过 CMakeLists.txt 配置文件指定编译参数,让 pevents 启用不同的功能

1
set(CMAKE_CXX_FLAGS "-std=c++11 -lpthread -DWFMO")

pevents 运行示例代码

提示

值得一提的是,pevents 的核心 C++ 源文件是 pevents.hpevents.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 拉取代码
$ git clone git@github.com:clay-world/pevents.git

# 进入源码目录
$ cd pevents

# 生成构建的输出目录
$ meson build

# 进入构建的输出目录
$ cd build

# 编译代码
$ ninja

# 运行示例程序
$ ./sample

pevents 的实战案例

编译说明

下面给出的案例使用了 pthread,由于 pthread 不是 Linux 系统默认的库,因此链接时需要使用静态库 libpthread.a。简而言之,在使用 pthread_create() 创建线程,以及调用 pthread_atfork() 函数建立 fork 处理程序时,需要通过 -lpthread 参数链接该库,同时还需要在 C++ 源文件里添加头文件 pthread.h

提示

为了可以正常编译使用了 pthread 的项目代码,不同构建工具的使用说明如下:

若使用 G++ 编译 C++ 项目,则编译命令的示例如下:

1
2
# 编译代码
$ g++ main.cpp -o main -lpthread

若使用 CMake 构建 C++ 项目,则 CMakeLists.txt 配置文件的示例内容如下:

1
2
3
set(CMAKE_CXX_FLAGS "-std=c++11 -lpthread -DWFMO")

add_executable(main main.cpp)

实战案例一

CreateEvent(true, true) - 手动清除事件信号,初始状态为有信号,点击下载 基于 CMake 构建的完整案例代码

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
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "pevents.h"

using namespace std;
using namespace neosmart;

neosmart_event_t g_hEvent = NULL;

void printIds(const char *s) {
pid_t pid = getpid();
pthread_t tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int) pid, (unsigned int) tid, (unsigned int) tid);
}

void *procFunc1(void *args) {
printIds("thread-1");
if (WaitForEvent(g_hEvent, 1) == 0) {
cout << "thread-1 is working..." << endl;
}
return ((void *) 0);
}

void *procFunc2(void *args) {
printIds("thread-2");
if (WaitForEvent(g_hEvent, 1) == 0) {
cout << "thread-2 is working..." << endl;
}
return ((void *) 0);
}

int main() {
// 手动清除事件信号,初始状态为有信号
g_hEvent = CreateEvent(true, true);

pthread_t ntid1;
pthread_create(&ntid1, NULL, procFunc1, NULL);

sleep(1);

pthread_t ntid2;
pthread_create(&ntid2, NULL, procFunc2, NULL);

sleep(5);
}

程序运行的结果如下:

1
2
3
4
thread-1 pid 62705 tid 2336241408 (0x8b403700)
thread-1 is working...
thread-2 pid 62705 tid 2327848704 (0x8ac02700)
thread-2 is working...

提示

可以看到线程 1 和线程 2 都完整执行了,这是因为创建的事件是需手动 Reset 才会变为无信号的,所以执行完线程 1 后事件仍处于有信号的状态,所以线程 2 的逻辑才会被继续执行。

实战案例二

CreateEvent(false, true) - 自动清除事件信号,且初始状态为有信号,点击下载 基于 CMake 构建的完整案例代码

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
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "pevents.h"

using namespace std;
using namespace neosmart;

neosmart_event_t g_hEvent = NULL;

void printIds(const char *s) {
pid_t pid = getpid();
pthread_t tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int) pid, (unsigned int) tid, (unsigned int) tid);
}

void *procFunc1(void *args) {
printIds("thread-1");
if (WaitForEvent(g_hEvent, 1) == 0) {
cout << "thread-1 is working..." << endl;
}
return ((void *) 0);
}

void *procFunc2(void *args) {
printIds("thread-2");
if (WaitForEvent(g_hEvent, 1) == 0) {
cout << "thread-2 is working..." << endl;
}
return ((void *) 0);
}

int main() {
// 自动清除事件信号,初始状态为有信号
g_hEvent = CreateEvent(false, true);

pthread_t ntid1;
pthread_create(&ntid1, NULL, procFunc1, NULL);

sleep(1);

pthread_t ntid2;
pthread_create(&ntid2, NULL, procFunc2, NULL);

sleep(5);
}

程序运行的结果如下:

1
2
3
thread-1 pid 59685 tid 2245932800 (0x85de3700)
thread-1 is working...
thread-2 pid 59685 tid 2237540096 (0x855e2700)

提示

可以看到只有线程 1 完整执行了,这是由于事件在执行完线程 1 后被系统自动重置为无信号,所以线程 2 中的逻辑没有被执行。

实战案例三

CreateEvent(true, false) - 手动清除事件信号,初始状态为无信号,包括 SetEvent()ResetEvent() 的使用,点击下载 基于 CMake 构建的完整案例代码

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
60
61
62
63
64
65
66
67
68
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "pevents.h"

using namespace std;
using namespace neosmart;

neosmart_event_t g_hEvent = NULL;

void printIds(const char *s) {
pid_t pid = getpid();
pthread_t tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int) pid, (unsigned int) tid, (unsigned int) tid);
}

void *procFunc1(void *args) {
printIds("thread-1");
if (WaitForEvent(g_hEvent, 1) == 0) {
cout << "thread-1 is working..." << endl;
}
// 重置事件为无信号
ResetEvent(g_hEvent);
return ((void *) 0);
}

void *procFunc2(void *args) {
printIds("thread-2");
if (WaitForEvent(g_hEvent, 1) == 0) {
cout << "thread-2 is working..." << endl;
}
return ((void *) 0);
}

void func1() {
// 手动清除事件信号,初始状态为有信号
g_hEvent = CreateEvent(true, true);

pthread_t ntid1;
pthread_create(&ntid1, NULL, procFunc1, NULL);

sleep(1);

pthread_t ntid2;
pthread_create(&ntid2, NULL, procFunc2, NULL);

sleep(5);
}

int main() {
// 手动清除事件信号,初始状态为无信号
g_hEvent = CreateEvent(true, false);

// 设置事件为有信号
SetEvent(g_hEvent);

pthread_t ntid1;
pthread_create(&ntid1, NULL, procFunc1, NULL);

sleep(1);

pthread_t ntid2;
pthread_create(&ntid2, NULL, procFunc2, NULL);

sleep(5);

return 0;
}

程序运行的结果如下:

1
2
3
thread-1 pid 70368 tid 2745513728 (0xa3a53700)
thread-1 is working...
thread-2 pid 70368 tid 2737121024 (0xa3252700)

提示

可以看到只有线程 1 完整执行了,这是因为线程 1 在执行之前事件是有信号的,执行完成后事件被手动重置为无信号,所以线程 2 中的逻辑没有被执行。

参考资料