关于eventfd,epoll,线程间通信小记

先介绍eventfd

1 #include<sys/eventfd.h>
2 int eventfd(unsigned int initval, int flags);

使用这个函数来创建一个事件对象,linux线程间通信为了提高效率,大多使用异步通信,采用事件监听和回调函数的方式来实现高效的任务处理方式(虽然会将逻辑变得复杂)。

linux内核会为这个事件对象维护一个64位的计数器(uint64_t).并在初始化时用传进去的initval来初始化这个计数器,然后返回一个文件描述符来代表这个事件对象。

第二个参数是描述这个事件对象的属性,可以设置为EFD_NONBLOCK , EFD_CLOEXEC;前面的是设置对象为非阻塞状态,如果没有设置为非阻塞状态,read系统调用来读这个计数器,且计数器的值为0时,就会一直阻塞在read系统调用上,反之如果设置了该标志位,就会返回EAGAIN错误。后面的EFD_CLOEXEC功能是在程序调用exec()函数族加载其他程序时自动关闭当前已有的文件描述符(具体为什么暂不解释)。

通过此函数得到的对象既然是一个计数器,我们就可以对它进行读和写:

使用write将缓冲区写入的8字节整形值加到内核计数器上。

使用read将内核计数的8字节值读取到缓冲区中,并把计数器重设为0,如果buffer的长度小于8字节则read会失败,错误码设为EINVAl。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

再介绍epoll,忍不住的可以直接向下翻

epoll是对select,poll这种IO多路转接方式的改进

接口:  int epoll_create(int intsize);

          int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

          int epoll_wait(int epfd, struct epoll_event* events,int maxevents, int timeout);

工作模式:

  水平触发:缺省的工作方式,并且同时支持block和no-blocksocket,在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表

  边缘触发:高速工作方式,只支持no-blocksocket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了

用途:使用epoll_wait对某个文件描述符进行事件监听,监听到事件后会返回相关的结构体,得到其中有事件到来的fd,使用对应的回调函数(手动实现fd到回调函数的映射)来处理该fd上的事件:读数据或者写数据之类的。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

下面进入使用场景:

原始做法:(会有bug,下面分析)

初始化:先生成一个eventfd,初始化计数器为0,此eventfd可以通过一些方法在下面两个线程间共享

线程A:处理一些来自外部的请求,每处理完一个请求后会向eventfd的计数器中写入处理的结果,是一个整型值,然后接着处理下一个请求。

线程B:对eventfd进行Epoll监听,回调函数的功能是对eventfd的计数器读数据出来并将结果进行分发。

用例1:外部单个客户端每隔1秒向线程A发送一个请求。

用例1结果:线程A正确处理请求,并将结果写入eventfd中,线程B及时从eventfd中读取出请求处理结果,并正确分发给其他线程。

用例2:外部单个客户端连续向线程A发送多个请求。

用例2结果:线程A正确处理请求,并正确地将结果写入eventfd中,但在一定概率的情况下,线程B从eventfd中读到的结果不是线程A一次写入的结果,而是多次写入的结果。因此不能正确的分发请求。线程B中epoll捕捉到的事件次数小于线程A写入产生的事件数量。

用例3:外部多个客户端同时向线程A发送一个请求

用例3结果:线程A正确处理请求,并正确的将结果写入eventfd中,在很大的概率情况下,线程B中eventfd中读到的结果不是线程A一次写入的结果,而是多次写入的结果。因此,也不能正确的分发请求。线程B中epoll捕捉到的事件次数小于线程A写入产生的事件数量。

BUG分析:在这个场景中,线程A和线程B分别相当于生产者和消费者,只从原始生产者消费者模型上看并没有问题,满足数据为空时读不到数据,数据满时写不进数据(read,write的功能),但是在当前场景中,加了一个特别的要求:每次写入的数据应该可以被独立识别而不是累加,每次写入的事件也应该被epoll独立的捕捉到。因此,需要对事件和数据各自进行序列化上的拆分。

改进做法:

初始化:先生成一个eventfd,初始化计数器为1,再生成一个空队列Q和互斥锁,此eventfd,队列Q和互斥锁可以通过一些方法在下面两个线程间共享,

线程A:处理一些来自外部的请求,每处理完一个请求后会从eventfd的计数器read数据,加1之后再write,将处理结果写入到队列末尾,然后接着处理下一个请求。

线程B:对eventfd进行Epoll监听,回调函数的功能是对eventfd的计数器read数据出来然后判断,如果大于1就自减1然后从队列头部取出数据,并将结果进行分发
,最后再写入新的计数器数据。如果等于1那么就直接返回,代表没有新的数据到来。

用例1,2,3在此环境下均可正常跑通。

回过头来分析原始做法的fatal error在哪:

作为生产者的线程A没有向线程B解释自己向eventfd中写入了多少个数据,产生了多少次事件。

作为消费者的线程B一次read就把eventfd中所有的数据当做一个数据读了出来,却没有相关依据来对读出来的数据做拆分。

作为通信工具的eventfd只能将数据进行累加,起到计数器的作用而不能存储实际数据。

作为消息监听的epoll在水平触发模式下只能通知是否有事件而不能通知有多少事件,在边缘触发下不能保留每次事件的产生都能及时被消费者捕获到。

因此,改进做法是将事件的多少通过计数器来表达,将实际传输的数据通过FIFO队列来传达。

Happy Ending Every Day.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏开源优测

python selenium2示例 - 同步机制

前言 在使用python selenium2进行自动化测试实践的过程中,经常会遇到元素定位不到,弹出框定位不到等等各种定位不到的情况,在大多数的情况下,无非是以...

2694
来自专栏屈定‘s Blog

Java学习记录--委派模型与类加载器

最近在读许令波的深入分析Java Web技术内幕一书,对于学习Java以来一直有的几个疑惑得到了解答,遂记录下来.

847
来自专栏阿杜的世界

Java Web技术经验总结(六)

这个函数中的关键是几个If...else...语句,通过判断指定的类是否存在,来决定是否添加对应的messageConverter(在4.0之后应该可以使用@C...

552
来自专栏流媒体

Linux下Socket编程(四)——epoll的使用简介

相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,...

732
来自专栏微信公众号:Java团长

Java NIO:NIO概述

在上一篇博文中讲述了几种IO模型,现在我们开始进入Java NIO编程主题。NIO是Java 4里面提供的新的API,目的是用来解决传统IO的问题。本文下面分别...

701
来自专栏nnngu

百度搜索 “Java面试题” 前200页(面试必看)

本文中的题目来源于网上的一篇文章《百度搜索 “Java面试题” 前200页》,但该文章里面只有题目,没有答案。因此,我整理了一些答案发布于本文。本文整理答案的原...

58511
来自专栏好好学java的技术栈

各大公司Java后端开发面试题总结

Java虚拟机规范中将Java运行时数据分为六种。 1.程序计数器:是一个数据结构,用于保存当前正常执行的程序的内存地址。Java虚拟机的多线程就是通过线程轮流...

1075
来自专栏程序员宝库

PHP7 下的协程实现

前言 相信大家都听说过『协程』这个概念吧。 但是有些同学对这个概念似懂非懂,不知道怎么实现,怎么用,用在哪,甚至有些人认为yield就是协程! 我始终相信,如果...

2766
来自专栏木宛城主

Unity应用架构设计(10)——绕不开的协程和多线程(Part 1)

在进入本章主题之前,我们必须要了解客户端应用程序都是单线程模型,即只有一个主线程(Main Thread),或者叫做UI线程,即所有的UI控件的创建和操作都是...

3476
来自专栏Golang语言社区

一些Golang小技巧

今天给大家介绍3个我觉得比较有启发的Golang小技巧,分别是以下几个代码片段 nsq里的select写文件和socket io模块里的sendfile fas...

3509

扫码关注云+社区