C语言event如何实现线程同步?

99ANYc3cd6
预计阅读时长 23 分钟
位置: 首页 C语言 正文

在 C 语言中,"event" 并不是一个像 intchar 那样的内置关键字或数据类型,它是一个编程概念设计模式,用于描述系统中发生的事情或状态的变化,你的程序需要能够“监听”这些事件,并在事件发生时做出相应的“响应”。

c语言 event
(图片来源网络,侵删)

理解事件的核心是理解“发生什么”“如何响应”


事件的核心思想:观察者模式

事件模型通常遵循“观察者模式”(Observer Pattern):

  • 事件源:能够产生事件的对象或模块,一个鼠标、一个网络套接字、一个定时器。
  • 事件:由事件源发出的通知,表示“某事发生了”。“鼠标被点击了”、“数据包到达了”、“1秒时间到了”。
  • 事件监听器/回调函数:程序员预先定义好的函数,用于处理特定事件,当事件发生时,事件源会“调用”这个函数。

在 C 语言中,我们通过函数指针来实现这个“回调”机制。


C 语言中实现事件模型的几种常见方式

根据应用场景的不同,C 语言实现事件的方式也多种多样,从简单的回调到复杂的事件循环。

c语言 event
(图片来源网络,侵删)

简单的函数指针回调

这是最基础、最直接的方式,当你调用一个库函数时,可以传入一个函数指针,当该函数完成某个特定操作后,会通过这个指针来通知你。

示例:一个简单的“任务完成”事件

假设我们有一个函数 do_long_task(),它执行一个耗时操作,我们想在它完成后立即通知主程序。

#include <stdio.h>
// 1. 定义回调函数的类型
//    它接收一个 int 参数,代表任务的结果
typedef void (*TaskCompletionCallback)(int result);
// 2. 模拟一个耗时任务
//    它接收一个回调函数指针作为参数
void do_long_task(TaskCompletionCallback callback) {
    printf("任务开始执行...\n");
    // 模拟耗时操作
    for (int i = 0; i < 5; i++) {
        printf(".");
        // 简单的延时,实际中可能用 sleep() 等
        for (int j = 0; j < 100000000; j++);
    }
    printf("\n任务执行完毕!\n");
    // 3. 事件发生时,调用回调函数
    printf("正在通知调用者...\n");
    callback(42); // 传递结果
}
// 4. 定义事件处理函数(即回调函数)
void on_task_complete(int result) {
    printf("【事件处理】收到任务完成通知!结果是: %d\n", result);
}
int main() {
    printf("主程序:准备启动任务,\n");
    // 5. 将事件处理函数的地址传递给 do_long_task
    do_long_task(on_task_complete);
    printf("主程序:任务已启动,继续做其他事情...\n");
    return 0;
}

输出:

主程序:准备启动任务。
任务开始执行....
任务执行完毕!
正在通知调用者...
【事件处理】收到任务完成通知!结果是: 42
主程序:任务已启动,继续做其他事情...

在这个例子中,on_task_complete 就是我们的“事件处理器”。do_long_task 是“事件源”,它在内部“触发”了 on_task_complete 这个事件。


基于 select / poll / epoll 的 I/O 事件多路复用

这是网络编程和服务器开发中最核心的事件模型,当你需要同时处理多个 I/O 源(如多个网络连接、文件描述符)时,不能为每个源都创建一个线程(开销太大),这时,事件循环就派上用场了。

核心思想:

  1. 创建一个“事件循环”。
  2. 告诉系统:“请帮我监视这些文件描述符(socket),我对它们的‘可读’、‘可写’或‘异常’状态感兴趣。”
  3. 程序进入阻塞状态,等待系统通知。
  4. 当任何一个被监视的文件描述符准备好(有数据可读),系统会唤醒程序,并返回哪些文件描述符发生了哪些事件。
  5. 程序根据返回的事件,调用相应的处理函数。

示例:使用 select 的伪代码

#include <stdio.h>
#include <sys/select.h>
#define MAX_FD 10
// 假设我们有一些文件描述符
int fds[MAX_FD];
// 事件处理函数
void handle_read_event(int fd) {
    printf("【事件处理】文件描述符 %d 已准备好读取!\n", fd);
    // ... 执行读取操作 ...
}
void handle_write_event(int fd) {
    printf("【事件处理】文件描述符 %d 已准备好写入!\n", fd);
    // ... 执行写入操作 ...
}
int main() {
    // 1. 初始化 fds,并设置要监视的 fd
    // ... (代码略) ...
    while (1) {
        // 2. 创建文件描述符集合
        fd_set read_fds, write_fds;
        FD_ZERO(&read_fds);
        FD_ZERO(&write_fds);
        // 3. 将需要监视的 fd 添加到集合中
        for (int i = 0; i < MAX_FD; i++) {
            // 假设 fds[i] 是一个有效的 socket
            FD_SET(fds[i], &read_fds); // 监视可读事件
            FD_SET(fds[i], &write_fds); // 监视可写事件
        }
        // 4. 调用 select,等待事件发生
        //    select 会阻塞,直到有事件发生或超时
        int active_count = select(MAX_FD, &read_fds, &write_fds, NULL, NULL);
        if (active_count > 0) {
            // 5. 检查哪些 fd 发生了事件
            for (int i = 0; i < MAX_FD; i++) {
                if (FD_ISSET(fds[i], &read_fds)) {
                    // 可读事件发生
                    handle_read_event(fds[i]);
                }
                if (FD_ISSET(fds[i], &write_fds)) {
                    // 可写事件发生
                    handle_write_event(fds[i]);
                }
            }
        }
    }
    return 0;
}

select 是较老的方式,poll 改进了一些,而 epoll (Linux) 和 kqueue (BSD/macOS) 是更高效、可扩展性更强的现代实现,它们是实现高性能网络服务器的基石。


使用第三方事件库

为了简化复杂的事件循环编程,许多优秀的第三方库应运而生,它们封装了底层 select/epoll 的细节,提供了更高级、更易用的 API。

著名例子:Libevent

Libevent 是一个强大的跨平台事件库,它提供了一个事件循环,可以统一处理 I/O 事件、定时器事件和信号事件。

使用 Libevent 的简化概念:

#include <event2/event.h>
#include <stdio.h>
// 1. 定义回调函数
void timer_cb(evutil_socket_t fd, short event, void *arg) {
    printf("【事件处理】定时器事件触发!\n");
    // 可以在这里做一些事情,然后再次添加定时器以实现周期性触发
}
int main() {
    // 2. 创建事件基 (event base)
    struct event_base *base = event_base_new();
    if (!base) {
        perror("event_base_new");
        return 1;
    }
    // 3. 创建一个定时器事件
    //    2秒后触发,一次性触发
    struct event *ev_timer = evtimer_new(base, timer_cb, NULL);
    struct timeval two_sec = {2, 0}; // 2 秒 0 微秒
    evtimer_add(ev_timer, &two_sec);
    printf("事件循环启动,2秒后将触发定时器事件...\n");
    // 4. 启动事件循环
    //    event_base_dispatch 会一直阻塞,直到没有活动的事件或被停止
    event_base_dispatch(base);
    // 5. 清理资源
    event_free(ev_timer);
    event_base_free(base);
    return 0;
}

这个例子展示了 Libevent 如何优雅地处理定时器事件,你只需要定义回调函数,然后将事件“添加”到事件基中,最后调用 event_base_dispatch 即可,库会自动处理底层的事件检测和分发。


C++ 中的 std::functionstd::bind

虽然问题问的是 C 语言,但了解一下 C++ 的实现有助于理解现代事件处理的便利性。

C++11 引入了 std::functionstd::bind(或 lambda 表达式),它们让回调函数的使用变得更加安全和灵活。

#include <iostream>
#include <functional> // 包含 std::function
#include <thread>
#include <chrono>
// 使用 std::function 定义回调类型
using TaskCallback = std::function<void(int)>;
void do_task_cpp(TaskCallback callback) {
    std::cout << "C++ 任务开始..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "C++ 任务完成!" << std::endl;
    callback(100); // 触发事件
}
// 事件处理函数
void on_complete_cpp(int result) {
    std::cout << "【C++ 事件处理】收到结果: " << result << std::endl;
}
int main() {
    // 使用 lambda 表达式作为事件处理器,非常方便
    TaskCallback my_callback = [](int res) {
        std::cout << "【Lambda 事件处理】结果: " << res << std::endl;
    };
    do_task_cpp(my_callback); // 传递 lambda
    do_task_cpp(on_complete_cpp); // 传递普通函数
    return 0;
}

std::function 可以存储、复制和调用任何可调用对象(函数指针、lambda、成员函数等),比 C 语言的函数指针功能强大得多。


特性 C 语言 (函数指针/select) C++ (std::function)
核心概念 回调、事件循环 回调、事件循环
实现方式 函数指针、select/epoll std::functionstd::bind、Lambda
优点 - 基础,直接,不依赖库
- 高效,epoll 性能极佳
- 类型安全
- 灵活,支持 lambda 和成员函数
- 代码更简洁易读
缺点 - 类型不安全,容易出错
- 处理复杂事件循环时代码繁琐
- 有一定的运行时开销
- 需要 C++11 或更高版本
适用场景 - 内核开发
- 嵌入式系统
- 高性能网络服务器(直接用 epoll
- 应用程序开发
- 需要复杂回调逻辑的场景
- GUI 开发

在 C 语言中,"event" 是一个通过函数指针和特定系统调用(如 select)来实现的核心编程范式,它让你能够构建高效、响应迅速的系统,尤其是在处理 I/O 和并发时。

-- 展开阅读全文 --
头像
C语言barrier是什么?如何使用?
« 上一篇 04-12
不显示,怎么办?
下一篇 » 04-12

相关文章

取消
微信二维码
支付宝二维码

目录[+]