何为同步与异步
在LNMP的生态当中我们基本上用到的是同步操作.例如PHP的
file_get_contents
函数就是一个典型的同步任务.而Node.js当中的回调模式是一个典型的异步模式如Promise
.
同步
同步可以理解为:发送一个系统调用,并等待系统调用的返回.
异步
异步可以理解为:发送一个系统调用,不等待系统返回可以继续处理当前的任务,等待系统处理完毕的回调.
不同的IO模型
阻塞与非阻塞强调的是当前进程或者是线程的状态.
阻塞IO
从图中可知,数据经历两种变化.
1.数据从没有准备状态到准备完成
2.从内核态拷贝数据到用户态.
非阻塞IO
发起recvfrom调用后内核在准备数据.
同步不断的轮训,发现数据准备完成后,将数据从内核态拷贝到用户态.
多路复用IO
IO多路复用:无需使用polling或者多线程就可以"同时"(指时间段)处理多个文件描述符.但是需要注意从内核读取数据还是同步的.
经典的select与epoll都是IO多路复用.图中描述的是select模型.epoll询问方式和select有很大不同.
epoll与select的区别
- epoll要比select高效
- select的支持最大文件描述符有限,epoll没有限制
select:每次返回准备好的文件描述数量.调用方自己遍历所有的FD,判断是否准备好,准备好然后再读数据.
select的最大的问题,我们不知道那些FD是否准备好,只能去遍历.
假设有100个FD,只有1个准备好,可想而知,相当于一个顺序查找,效率低下.
因此高效的epoll就出现了,只告诉调用方准备好的FD
fd_set readfds, writefds;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
for (int i=0; i < read_fd_count; i++)
FD_SET(my_read_fds[i], &readfds);
for (int i=0; i < write_fd_count; i++)
FD_SET(my_write_fds[i], &writefds);
struct timeval timeout;
timeout.tv_sec = 3;
timeout.tv_usec = 0;
int num_ready = select(FD_SETSIZE, &readfds, &writefds, NULL, &timeout);
if (num_ready < 0) {
perror("error in select()");
} else if (num_ready == 0) {
printf("timeout\n");
} else {
for (int i=0; i < read_fd_count; i++)
if (FD_ISSET(my_read_fds[i], &readfds))
printf("fd %d is ready for reading\n", my_read_fds[i]);
for (int i=0; i < write_fd_count; i++)
if (FD_ISSET(my_write_fds[i], &writefds))
printf("fd %d is ready for writing\n", my_write_fds[i]);
}
epoll不是POSIX标准.但却在Linux当中被广泛使用.
epoll:只返会准备好的FD.
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* Code to set up listening socket, 'listen_sock',
(socket(), bind(), listen()) omitted */
epollfd = epoll_create1(0);//创建epoll
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {//监听指定的listen_sock
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);//循环取事件,放入event事件
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_sock) {
conn_sock = accept(listen_sock,
(struct sockaddr *) &addr, &addrlen);//接收客户端连接
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
setnonblocking(conn_sock);// ET模式下需要设置为非阻塞模式
ev.events = EPOLLIN | EPOLLET;// ET模式下
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {//添加客户端的事件到当前的epoll
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd); //使用新的准备好的文件描述符不停接收数据,并在这里记录当前读取,写入的数据
}
}
}
信号驱动IO
信号驱动IO无需等待数据准备好,系统会发送信号SIGIO回调handler,然后读取数据.
这时候IO是同步读取,从内核态到用户态.
异步IO
真正的异步IO,从图中很明显看出数据已经被拷贝到了用户态,这是与信号IO模型最大的区别.
IO模型比较
从图中我可以得出:
- 异步IO:不会导致线程阻塞.
- 同步IO:线程一直阻塞到数据IO操作完成.