手写Looper(一)---- 使用eventfd

1,823 阅读5分钟

手写Looper(一)---- 使用eventfd

作为Android开发,Handler想必不可能陌生,随口就能说出Handler、Looper、MessageQueue三者之间的羁绊。那么,Looper作为事件驱动器,一直循环读取信息,其具体又是如何去实现的呢?其实Handler机制本身也属于生产者消费者模式,对于生产者消费者模式,最关键的就是等待通知。用java我们随手就能利用wait/notify机制写出来。但是通过对Looper.java的源码分析,可以看出其并非是wait/notify机制,而是调用的native层的Looper.cpp去实现的。

eventfd

eventfd是linux提供的用于事件通知的一种文件描述符,是可以用来实现wait/notify的一种工具,用其可以轻易实现线程间或进程间的事件通知。Looper就是通过eventfd来实现的事件通知。需要注意的是eventfd虽然是一个文件描述符,但它本身并非是一个真实的文件。

eventfd内部维护了一个uint64_t类型的一个计数器,对它读写也只能是对其内部计数器进行读写,甚至可以理解为eventfd就是一个数字,对它读写就是对这个数字的赋值和取值。其计数器为0的时候表示文件不可读,为最大值的时候,表示不可写。对eventfd的写入,实际上并不是一个简单的赋值操作,而是累加操作。例如其内部计数器为1的时候,再写入一个3,此时其内部计数器则会为4。因此当计数器为最大值时,就不可再写入了,因为写入会导致溢出。

创建文件描述符 int eventfd(count, flag)

该方法用于创建一个文件描述符,其第一个参数是计数器的初始值,第二个参数是对应的flag,可以用‘或’操作设置多个flag。返回值则是该文件描述符。flag取以下三个值:

  1. EFD_NONBLOCK 使用该flag创建的文件描述符,对其读写的时候不会阻塞。默认情况下,对eventfd的读写操作是阻塞式的,即eventfd不可读写的时候,线程会阻塞在read、write方法中。而设置了该flag后,若是文件不可读写,则不会阻塞而是直接返回-1。
  2. EFD_SEMAPHORE 使用该flag创建的是属于信号量类型的文件描述符。默认情况下,对eventfd的读取操作,会直接读取到内部的计数器的值,读取后重置计数器为0,此时eventfd被置为不可读的状态。而设置了该flag后,读取的时候每次都只能读取到1,并且会将内部计数器值减1,若此时计数器仍不为0,则还是属于可读的。
  3. EFD_CLOEXEC 当通过exec执行其他程序后,将复制的eventfd自动关闭。

标准读写 write & read

#include <unistd.h>

ssize_t write (int __fd, const void *__buf, size_t __n)
ssize_t read (int __fd, void *__buf, size_t __nbytes)

对eventfd的读写就像是对普通文件描述符的读写一样没什么特殊的,但是对其读写的大小只能是uint64_t的长度。例如写入内容的话,只能写入uint64_t类型的数字,读取的话,也只能通过uint64_t类型进行接收,否则会读取或写入失败。返回值也是一样的,若是读写成功,则返回读写的大小,即sizeof(uint64_t),失败则返回-1。

默认情况下,读写都是阻塞式的。例如eventfd是不可读的时候,即内部计数器值为0,此时read方法会阻塞当前线程,直到可读的时候才会返回。可以通过创建eventfd的时候传入EFD_NONBLOCK的flag,此时则是非阻塞式的,若是不可读写,则并不会阻塞而是直接返回-1。

eventfd封装的读写 eventfd_write & eventfd_read

#include <sys/eventfd.h>

typedef uint64_t eventfd_t;

int eventfd_read (int __fd, eventfd_t *__value);
int eventfd_write (int __fd, eventfd_t __value);

除了标准read、write读写,eventfd还有一种读写的方式,即上述的两个方法,实际上也是对标准读写的封装。由于eventfd要求读写的类型必须是uint64_t类型,所以直接使用标准读写可能会出现长度类型的问题。而经过eventfd封装后的读写方法,明确指明了数据类型为eventfd_t类型(实际也是uint64_t类型),避免了写入数据错误的问题。返回值也做了修改,读写成功的话返回值为0,否则为-1。

使用方式示例

#include <sys/eventfd.h>  
#include <unistd.h>  
#include <stdio.h>  
#include <pthread.h>  
  
void *child_write(void *data) {  
    int *efd = data;  
    pthread_t tid = pthread_self();  
    // 子线程负责写入  
    for (int i = 0; i < 5; ++i) {  
        eventfd_t number = 3;  
        ssize_t value = write(*efd, &number, sizeof(uint64_t));  
        if (value == sizeof(uint64_t)) {  
             printf("Child Write Success, tid: %lu\n", tid);  
        } else {  
            printf("Child Write Fail, tid: %lu\n", tid);  
        }  
        sleep(1);  
    }  
    return 0;  
}  
  
  
int main(int argc, char *argv[]) {  
    int efd = eventfd(0, EFD_CLOEXEC);  
    if (efd < 0) {  
        printf("create efd error. \n");  
        return -1;  
    }  
    printf("efd: %d,\n", efd);  
    // 创建子线程写入  
    pthread_t child_thread;  
    pthread_create(&child_thread, 0, child_write, &efd);  
    if (child_thread < 0) {  
        printf("create thread error.\n");  
        close(efd);  
        return -1;  
    }  
    int count = 0;  
    while (1) {  
        uint64_t number = 0;  
        ssize_t len = read(efd, &number, sizeof(uint64_t));  
        if (len == sizeof(uint64_t)) {  
            printf("read success: %lu, len = %zd, count = %d\n", number, len, ++count);  
        } else {  
            printf("read fail.\n");  
        }  
    }
    // 并不会执行到这里,但是用完后需要即使关闭eventfd
    close(efd);  
    return 0;  
}

上述代码实现了在主线程循环读取数据,若是eventfd不可读的话,则会阻塞在read方法中。然后创建了一个子线程每隔1秒会向eventfd中写入一个值,写入后主线程会被唤醒,然后读取到这个值后打印并再次阻塞在read中等待。

image.png

总结

eventfd是用来实现wait/notify机制的,因此不要把它当成一个生产者消费者模型,即不要想着一个线程读取,多个线程写入,由于eventfd计数器的特性,每次写入的值会被累加,所以不适合多个线程同时写入。多个线程情况下,可能会出现写入多次,而只读取一次的情况。而若是想实现写入几次读取几次的话,则需要创建时传入EFD_SEMAPHORE的flag,但这样的话,每次读取的时候都只能读取到1,就无法区分线程写入的内容了。总而言之,eventfd的最佳使用环境就是睡眠唤醒。

因此想要直接使用eventfd去实现Looper还是不太合适的,因此还需要使用别的方式,例如epoll:手写Looper(二)---- 使用epoll