Libevent学习日记(一)
Libevent简介
libevent是一个开源的高性能轻量级linux c++ web库。
- 事件驱动,高性能;
- 轻量级,专注于网络;
- 跨平台,支持 Windows、Linux、Mac Os等;
- 支持多种 I/O多路复用技术, epoll、poll、dev/poll、select 和kqueue 等;
- 支持 I/O,定时器和信号等事件;
EPOLL 与 Reactor
服务器架构模型一般有三个部分组成:
- IO处理部分
- 逻辑处理部分
- 储存管理部分
我们要说的EPOLL就是IO处理模块的内容。IO处理模块负责接收客户端连接,接收消息发送消息等操作。那么我们说的发送消息,这个消息是什么样的呢?
流
流是可以进行I/O操作的内核对象,包括文件、socket、管道等等,都可以称之为流,它的入口是文件描述符【file descriptor】。
I/O操作就是在流中写入/读出数据。
阻塞,即暂时不能对流进行要做的操作,且不做其它操作,有以下两种情况,不占据时间片。
(1)流已写完,而我还想 write 进去。
(2)流是空的,而我还想 read 出来。
非阻塞-忙轮询:即在对流操作不能立刻进行时,不做等待,继续处理别的事,但是会进行轮询访问是否可以进行操作,耗费时间片。
while true {
for i in 流[] {
if i has 数据 {
读 或者 其他处理
}
}
}
I/O复用:select 、poll、epoll
I/O复用的思想就是避免阻塞,我们比如accept时要是没有客户端连接上来就会造成阻塞,这时候要是我们选择等到有连接时才调用accept就不会造成阻塞了。select就是监听一些文件描述符,当这些文件描述符上有事件发生时就会提醒用户。
但是请注意:select它们等待有事件发生这件事本身是阻塞的!不阻塞的是accept等操作。
while true {
select(流[]); //阻塞
for i in 流[] {
if i has 数据 {
读 或者 其他处理
}
}
}
对于select来说,每次都要将全部监听的文件描述符从内核态复制到用户态,非常的消耗时间和资源,但是它的好处就是可以同时监听多个socket。
poll实际上和select差不多,此处忽略不讲。
为了优化这个步骤,多年之后又出现了epoll,epoll每次都只将有事件发生的文件描述符复制到用户态,降低了资源的损耗。
while true {
可处理的流[] = epoll_wait(epoll_fd); //阻塞
for i in 可处理的流[] {
读 或者 其他处理
}
}
EPOLL API
epoll的函数很少,只有三个。
1、创建epoll。
/**
* @param size 告诉内核监听的数目
*
* @returns 返回一个epoll句柄(即一个文件描述符)
*/
int epoll_create(int size);
2、设置epoll状态。
/**
* @param epfd 用epoll_create所创建的epoll句柄
* @param op 表示对epoll监控描述符控制的动作
*
* EPOLL_CTL_ADD(注册新的fd到epfd)
* EPOLL_CTL_MOD(修改已经注册的fd的监听事件)
* EPOLL_CTL_DEL(epfd删除一个fd)
*
* @param fd 需要监听的文件描述符
* @param event 告诉内核需要监听的事件
*
* @returns 成功返回0,失败返回-1, errno查看错误信息
*/
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event);
struct epoll_event {
__uint32_t events; /* epoll 事件 */
epoll_data_t data; /* 用户传递的数据 */
}
/*
* events : {EPOLLIN, EPOLLOUT, EPOLLPRI,
EPOLLHUP, EPOLLET, EPOLLONESHOT}
*/
typedef union epoll_data {
void *ptr;//指定与fd相关的用户数据,包含fd
int fd;//从属目标的文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
下面看一个将sockfd为5的文件描述符添加到epoll监听事件时发生了什么。
struct epoll_event new_event;
new_event.events = EPOLLIN | EPOLLOUT;
new_event.data.fd = 5;
epoll_ctl(epfd, EPOLL_CTL_ADD, 5, &new_event);
这里可以看到,我们使用了一个epoll_event结构体来封装一个事件。首先将events设置为EPOLLIN | EPOLLOUT 就表示我们监听的是对端有写入(可读)或对端读出(可写)事件。然后是绑定这个事件的文件描述符。
最后调用epoll_ctl将该事件放入epoll监听队列。
3、等待epoll,获得活跃的文件描述符集。
/**
*
* @param epfd 用epoll_create所创建的epoll句柄
* @param event 从内核得到的事件集合
* @param maxevents 告知内核这个events有多大,
* 注意: 值 不能大于创建epoll_create()时的size.
* @param timeout 超时时间
* -1: 永久阻塞
* 0: 立即返回,非阻塞
* >0: 指定微秒
*
* @returns 成功: 有多少文件描述符就绪,时间到时返回0
* 失败: -1, errno 查看错误
*/
int epoll_wait(int epfd, struct epoll_event *event,
int maxevents, int timeout);
因此,整个epoll流程如下:
//创建 epoll
int epfd = epoll_crete(1000);
//将 listen_fd 添加进 epoll 中
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd,&listen_event);
while (1) {
//阻塞等待 epoll 中 的fd 触发
int active_cnt = epoll_wait(epfd, events, 1000, -1);
for (i = 0 ; i < active_cnt; i++) {
if (evnets[i].data.fd == listen_fd) {
//accept. 并且将新accept 的fd 加进epoll中.
}
else if (events[i].events & EPOLLIN) {
//对此fd 进行读操作
}
else if (events[i].events & EPOLLOUT) {
//对此fd 进行写操作
}
}
}
触发机制
说到这里,我们前面提过,当有文件描述符活跃时,就会触发从内核态向用户态的文件描述符集复制行为。
select支持的就是每当有还没处理的活跃的文件描述符,就会隔一段时间重新完成一次内核态到用户态的复制,不断提醒,这就是水平触发。
虽然水平触发效率不高,但是它的安全性能好,不会漏掉任何一个活跃事件。
只要event_A的事件没被处理掉,就会不停的将其复制过去提醒用户。
为了提高效率,EPOLL机制提出了边缘触发方法,我们只在事件第一次活跃时提醒用户,不管用户处理不处理,没有新活跃事件例如写入新的数据到流前都不提醒了。
边缘触发,相对跟水平触发相反,当内核有事件到达, 只会通知用户一次,至于用户处理还是不处理,以后将不会再通知。这样减少了拷贝过程,增加了性能,但是相对来说,如果用户马虎忘记处理,将会产生事件丢的情况。
下面来看一个简单的epoll服务器和客户端。
EPOLL 服务端
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define SERVER_PORT (7778)
#define EPOLL_MAX_NUM (2048)
#define BUFFER_MAX_LEN (4096)
char buffer[BUFFER_MAX_LEN];
void str_toupper(char *str)
{
int i;
for (i = 0; i < strlen(str); i ++) {
str[i] = toupper(str[i]);
}
}
int main(int argc, char **argv)
{
int listen_fd = 0;
int client_fd = 0;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
socklen_t client_len;
int epfd = 0;
struct epoll_event event, *my_events;
// socket
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
// bind
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
// listen
listen(listen_fd, 10);
// epoll create
epfd = epoll_create(EPOLL_MAX_NUM);
if (epfd < 0) {
perror("epoll create");
goto END;
}
// listen_fd -> epoll
event.events = EPOLLIN;
event.data.fd = listen_fd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &event) < 0) {
perror("epoll ctl add listen_fd ");
goto END;
}
my_events = malloc(sizeof(struct epoll_event) * EPOLL_MAX_NUM);
while (1) {
// epoll wait
int active_fds_cnt = epoll_wait(epfd, my_events, EPOLL_MAX_NUM, -1);
int i = 0;
for (i = 0; i < active_fds_cnt; i++) {
// if fd == listen_fd
if (my_events[i].data.fd == listen_fd) {
//accept
client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
continue;
}
char ip[20];
printf("new connection[%s:%d]\n", inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip)), ntohs(client_addr.sin_port));
event.events = EPOLLIN | EPOLLET;
event.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &event);
}
else if (my_events[i].events & EPOLLIN) {
printf("EPOLLIN\n");
client_fd = my_events[i].data.fd;
// do read
buffer[0] = '\0';
int n = read(client_fd, buffer, 5);
if (n < 0) {
perror("read");
continue;
}
else if (n == 0) {
epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &event);
close(client_fd);
}
else {
printf("[read]: %s\n", buffer);
buffer[n] = '\0';
#if 1
str_toupper(buffer);
write(client_fd, buffer, strlen(buffer));
printf("[write]: %s\n", buffer);
memset(buffer, 0, BUFFER_MAX_LEN);
#endif
/*
event.events = EPOLLOUT;
event.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event);
*/
}
}
else if (my_events[i].events & EPOLLOUT) {
printf("EPOLLOUT\n");
/*
client_fd = my_events[i].data.fd;
str_toupper(buffer);
write(client_fd, buffer, strlen(buffer));
printf("[write]: %s\n", buffer);
memset(buffer, 0, BUFFER_MAX_LEN);
event.events = EPOLLIN;
event.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &event);
*/
}
}
}
END:
close(epfd);
close(listen_fd);
return 0;
}
EPOLL客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#define MAX_LINE (1024)
#define SERVER_PORT (7778)
void setnoblocking(int fd)
{
int opts = 0;
opts = fcntl(fd, F_GETFL);
opts = opts | O_NONBLOCK;
fcntl(fd, F_SETFL);
}
int main(int argc, char **argv)
{
int sockfd;
char recvline[MAX_LINE + 1] = {0};
struct sockaddr_in server_addr;
if (argc != 2) {
fprintf(stderr, "usage ./client <SERVER_IP>\n");
exit(0);
}
// 创建socket
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
fprintf(stderr, "socket error");
exit(0);
}
// server addr 赋值
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0) {
fprintf(stderr, "inet_pton error for %s", argv[1]);
exit(0);
}
// 链接服务端
if (connect(sockfd, (struct sockaddr*) &server_addr, sizeof(server_addr)) < 0) {
perror("connect");
fprintf(stderr, "connect error\n");
exit(0);
}
setnoblocking(sockfd);
char input[100];
int n = 0;
int count = 0;
// 不断的从标准输入字符串
while (fgets(input, 100, stdin) != NULL)
{
printf("[send] %s\n", input);
n = 0;
// 把输入的字符串发送 到 服务器中去
n = send(sockfd, input, strlen(input), 0);
if (n < 0) {
perror("send");
}
n = 0;
count = 0;
// 读取 服务器返回的数据
while (1)
{
n = read(sockfd, recvline + count, MAX_LINE);
if (n == MAX_LINE)
{
count += n;
continue;
}
else if (n < 0){
perror("recv");
break;
}
else {
count += n;
recvline[count] = '\0';
printf("[recv] %s\n", recvline);
break;
}
}
}
return 0;
}
两种高效的事件处理模式
服务器经常需要处理三类事件:
- IO事件
- 信号事件
- 定时事件
Reactor
Reactor模式中,主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立刻通知工作线程(逻辑单元)。
在同步模型中(以epoll_wait为例),实现Reactor的工作流程是:
- 主线程往内核事件表上注册socket上的读就绪事件
- 主线程调用epoll_wait等待socket 上有数据可读
- 当socket上有数据可读时,epoll_wait通知主线程,主线程将事件放入请求列表
- 睡眠在请求队列上的某个工作线程被唤醒,从socket中读取数据,并处理客户请求,然后往内核事件表中注册该socket上的可就绪事件
- 主线程调用epoll_wait等待socket可写
- 当socket可写时,epoll_wait将socket可写事件放入请求队列
- 睡眠在工作队列上的某个工作线程被唤醒,往socket上写入服务器处理客户请求的结果
背景和问题:
在分布式系统尤其是服务器这一类事件驱动应用中,虽然这些请求最终会被序列化地处理,但是必须时刻准备着处理多个同时到来的服务请求。在实际应用中,这些请求总是通过一个事件(如CONNECTOR、READ、WRITE等)来表示的。在有序地处理这些服务请求之前,应用程序必须先分离和调度这些同时到达的事件。
为了有效地解决这个问题,我们需要做到以下4方面:
(1)为了提高系统的可测量性和反应时间,应用程序不能长时间阻塞在某个事件源上而停止对其他事件的处理,这样会严重降低对客户端的响应度。
(2) 为了提高吞吐量,任何没有必要的上下文切换、同步和CPU之间的数据移动都要避免。
(3)引进新的服务或改良已有的服务都要对既有的事件分离和调度机制带来尽可能小的影响。
(4)大量的应用程序代码需要隐藏在复杂的多线程和同步机制之后。
解决方案:
用select、poll、epoll等构建出一个异步的服务器。也就是说,在等待连接的时候是自由的而不是阻塞的。当连接请求到达,服务器调用回调函数建立连接。
两个web服务器例子。
HTTPHandle是事件处理器,主要负责事件来临之后进行的操作,Initiation Dispatcher核心就是用select、poll、epoll实现的,当有请求到达Initiation Dispacher会通知http handler数据到来,应该去读去解析了,基本是http handle主缆了大部分的工作。
Reactor结构
五个关键的参与者:
- 描述符(handle):由操作系统提供,用来识别每一个事件,如socket描述符、文件描述符等。
- 同步事件分离器(demultiplexer):一个函数,用于等待事件的发生。它会被阻塞,直到分离器分离的描述符集合上有事件发生。
- 事件处理接口(event handle):由一个或多个模板函数组成的接口。描述了对应事件的某个操作,例如读取,写出等。
- 具体的事件处理器:是事件处理器接口的实现。它实现了应用程序提供的某个服务。每个具体的事件处理器总和一个描述符相关。它使用描述符来识别事件、识别应用程序提供的服务。
- Reactor 管理器(reactor):定义了一些接口,用于应用程序控制事件调度,以及应用程序注册、删除事件处理器和相关的描述符。它是事件处理器的调度核心。 Reactor管理器使用同步事件分离器来等待事件的发生。一旦事件发生,Reactor管理器先是分离每个事件,然后调度事件处理器,最后调用相关的模 板函数来处理这个事件。
基于Reactor反应堆的实现
这也是libevent的实现原理。
#include <stdlib.h>
#include <stdio.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#define MAX_EVENTS 1024
#define BUFLEN 128
#define SERV_PORT 8888
/*
* status:1表示在监听事件中,0表示不在
* last_active:记录最后一次响应时间,做超时处理
*/
struct myevent_s
{
int fd; //cfd listenfd
int events; //EPOLLIN EPLLOUT
void *arg; //指向自己结构体指针
void (*call_back)(int fd, int events, void *arg);
int status;
char buf[BUFLEN];
int len;
long last_active;
};
int g_efd; /* epoll_create返回的句柄 */
struct myevent_s g_events[MAX_EVENTS+1]; /* +1 最后一个用于 listen fd */
/**
* @param ev :event in events
* @param fd :socket fd
* @etc:...
*/
void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg)
{
ev->fd = fd;
ev->call_back = call_back;
ev->events = 0;
ev->arg = arg;
ev->status = 0;
//memset(ev->buf, 0, sizeof(ev->buf));
//ev->len = 0;
ev->last_active = time(NULL);
return;
}
void recvdata(int fd, int events, void *arg);
void senddata(int fd, int events, void *arg);
void eventadd(int efd, int events, struct myevent_s *ev)
{
struct epoll_event epv = {0, {0}};
int op;
epv.data.ptr = ev;
epv.events = ev->events = events;
if (ev->status == 1)
{
op = EPOLL_CTL_MOD;
}
else
{
op = EPOLL_CTL_ADD;
ev->status = 1;
}
if (epoll_ctl(efd, op, ev->fd, &epv) < 0)
printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
else
printf("event add OK [fd=%d], op=%d, events[%0X]\n", ev->fd, op, events);
return;
}
void eventdel(int efd, struct myevent_s *ev)
{
struct epoll_event epv = {0, {0}};
if (ev->status != 1)
return;
epv.data.ptr = ev;
ev->status = 0;
epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv);
return;
}
void acceptconn(int lfd, int events, void *arg)
{
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
int cfd, i;
if ((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1)
{
if (errno != EAGAIN && errno != EINTR)
{
/* 暂时不做出错处理 */
}
printf("%s: accept, %s\n", __func__, strerror(errno));
return;
}
do {
for (i = 0; i < MAX_EVENTS; i++)
{//find a not listened event
if (g_events[i].status == 0)
break;
}
if (i == MAX_EVENTS)
{
printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
break;
}
int flag = 0;
if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0)//set clientfd not blocked
{
printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
break;
}
eventset(&g_events[i], cfd, recvdata, &g_events[i]);//receive client's data
eventadd(g_efd, EPOLLIN, &g_events[i]);//add event to epoll
} while(0);
printf("new connect [%s:%d][time:%ld], pos[%d]\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i);
return;
}
void recvdata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = recv(fd, ev->buf, sizeof(ev->buf), 0);
eventdel(g_efd, ev);
if (len > 0)
{
ev->len = len;
ev->buf[len] = '\0';
printf("C[%d]:%s\n", fd, ev->buf);
/* 转换为发送事件 */
eventset(ev, fd, senddata, ev);
eventadd(g_efd, EPOLLOUT, ev);
}
else if (len == 0)
{
close(ev->fd);
/* ev-g_events 地址相减得到偏移元素位置 */
printf("[fd=%d] pos[%d], closed\n", fd, (int)(ev - g_events));
}
else
{
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
}
return;
}
void senddata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = send(fd, ev->buf, ev->len, 0);
//printf("fd=%d\tev->buf=%s\ttev->len=%d\n", fd, ev->buf, ev->len);
//printf("send len = %d\n", len);
eventdel(g_efd, ev);
if (len > 0) {
printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf);
eventset(ev, fd, recvdata, ev);//set call_back to recvdata
eventadd(g_efd, EPOLLIN, ev);
}
else {
close(ev->fd);
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
return;
}
/**
* @param efd : handle return from epoll_create
* @param port : server port
* @func: init server ip and port ,prepare to accept connection
*/
void initlistensocket(int efd, short port)
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(lfd, F_SETFL, O_NONBLOCK);//set listenfd not blocked
eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]);//set call_back function is accepconn
eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);//
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(port);
bind(lfd, (struct sockaddr *)&sin, sizeof(sin));
listen(lfd, 20);
return;
}
/**
* (1)create socket and listen for connect
* (2)while
* check if connection overtime
*
*/
int main(int argc, char *argv[])
{
unsigned short port = SERV_PORT;//define server port
if (argc == 2)
port = atoi(argv[1]);
g_efd = epoll_create(MAX_EVENTS+1);//get epoll handle
if (g_efd <= 0)//epoll error
printf("create efd in %s err %s\n", __func__, strerror(errno));
initlistensocket(g_efd, port);
/* 事件循环 */
struct epoll_event events[MAX_EVENTS+1];
printf("server running:port[%d]\n", port);
int checkpos = 0, i;
while (1)
{
/* 超时验证,每次测试100个链接,不测试listenfd 当客户端60秒内没有和服务器通信,则关闭此客户端链接 */
long now = time(NULL);
for (i = 0; i < 100; i++, checkpos++)
{
if (checkpos == MAX_EVENTS)
checkpos = 0;
if (g_events[checkpos].status != 1)//not be listened
continue;
long duration = now - g_events[checkpos].last_active;//get not-active time
if (duration >= 60)
{
close(g_events[checkpos].fd);
printf("[fd=%d] timeout\n", g_events[checkpos].fd);
eventdel(g_efd, &g_events[checkpos]);//delete not-active socketfd
}
}
/* 等待事件发生 */
int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000);
if (nfd < 0)
{
printf("epoll_wait error, exit\n");
break;
}
for (i = 0; i < nfd; i++)
{
struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;
if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN))
{
ev->call_back(ev->fd, events[i].events, ev->arg);//use call_back function
}
if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT))
{
ev->call_back(ev->fd, events[i].events, ev->arg);
}
}
}
/* 退出前释放所有资源 */
return 0;
}
主线程(I/O处理模块)做的事:
- 建立socket等待连接
- epoll监听事件
- 超时连接处理
- 调动回调函数进行事件处理
Proactor
这里不做详细介绍,它就是将大部分操作交给内核和操作系统。
一些知识
阻塞、非阻塞、同步、异步
同步
当有一个事件没处理完,调用它的事件就只能等着。
比如A调用B,B还没处理完,A就要一直等待。
比如给书店老板打电话问有没有操作系统这本书,老板就去找,等到找到再告诉你,此时电话不停。
异步
调用另一个事件的事件时,另一个事件立刻返回,但是不包含结果。等到另一个事件处理完,再通过回调函数等获得结果。被调用者通过“状态”、“通知”、“回调”三种途径通知调用者。
例如书店老板先跟你挂了电话,找到书后再打给你(回调)。
阻塞
阻塞在于结果返回前,调用它的任务都不能做其他的事。
比如老板找书时你什么都不干了。
非阻塞
非阻塞在调用结果返回前可以先做自己的事。
比如老板找书时你又进入了峡谷。
所以同步异步区别在于消息通知的方式,阻塞非阻塞区别在于结果返回前调用者的状态。
回调函数
说到回调函数,我们先想想怎么改变algorithm 中的sort的排序规则?
在里面那个排序规则函数就是一个回调函数。
来看一段维基百科关于回调函数的介绍。
In computer programming, a callback is any executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at a given time. This execution may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback. --wikipedia
也就是说,一个函数被当成参数传递,在某个时刻才被调动,就是回调。要是立刻被调用,是同步回调,否则是异步回调。比如上面代码的acceptconn就是一个异步回调函数。
回调函数优点
- 解耦
当我们要调用不同的工作函数,只需要传入函数名就可以了,上面的代码很多处都体现了这一点。
程序传入函数当参数,在库里匹配函数,由库调用那个函数。
在回调中,主程序把回调函数像参数一样传入库函数。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能。
一段可执行的回调函数代码
#include<stdio.h>
int Callback_1() // Callback Function 1
{
printf("Hello, this is Callback_1 ");
return 0;
}
int Callback_2() // Callback Function 2
{
printf("Hello, this is Callback_2 ");
return 0;
}
int Callback_3() // Callback Function 3
{
printf("Hello, this is Callback_3 ");
return 0;
}
int Handle(int (*Callback)())
{
printf("Entering Handle Function. ");
Callback();
printf("Leaving Handle Function. ");
}
int main()
{
printf("Entering Main Function. ");
Handle(Callback_1);
Handle(Callback_2);
Handle(Callback_3);
printf("Leaving Main Function. ");
return 0;
}
带参数的回调函数代码
#include<stdio.h>
int Callback_1(int x) // Callback Function 1
{
printf("Hello, this is Callback_1: x = %d ", x);
return 0;
}
int Callback_2(int x) // Callback Function 2
{
printf("Hello, this is Callback_2: x = %d ", x);
return 0;
}
int Callback_3(int x) // Callback Function 3
{
printf("Hello, this is Callback_3: x = %d ", x);
return 0;
}
int Handle(int y, int (*Callback)(int))
{
printf("Entering Handle Function. ");
Callback(y);
printf("Leaving Handle Function. ");
}
int main()
{
int a = 2;
int b = 4;
int c = 6;
printf("Entering Main Function. ");
Handle(a, Callback_1);
Handle(b, Callback_2);
Handle(c, Callback_3);
printf("Leaving Main Function. ");
return 0;
}
下一篇关于一些API:Libevent学习日记(二)
参考资料
Libevent深入浅出 [作者:刘丹冰]
菜鸟教程 回调函数详解