同步异步分为两种:并发的同步异步 和 I/O的同步异步。
一、并发的同步和异步:
同步:完全按照代码顺序执行,前面执行完了,才能去执行后面的代码。所以当进程执行到某调用时,调用发出后,如果进程不能立刻得到结果,则等待,直到这个调用返回。
例如:普通模式的B/S模式:提交请求->等待服务器处理->处理完毕返回。这个期间客户端不能做任何事。
异步:彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完后才能后再工作,在等待的事件结束后,可以再返回去执行。所以当调用发出后,若调用者不能立刻得到结果,则是实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。线程就是实现异步的一个方式,
二、阻塞和非阻塞:
阻塞和非阻塞强调的是应用程序在等待调用结果(消息,返回值)时的状态.
阻塞:在调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,CPU不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
和同步的区别:对于同步来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。即同步等待时什么都不干,白白占用着资源。
非阻塞:一个调用过程会立刻返回,无论结果怎样(有结果就返回结果,没有结果就返回错误。总之,它不会阻塞当前线程,而是会立刻返回)。
三、I/O的同步和异步:
对文件的读写,都是通过调用内核提供的系统调用,内核给我们返回一个文件描述符;应用程序通过对文件描述符的读写来进行对文件的读写。
以读操作为例:通过read系统调用进行的I/O 操作会陷入到内核,由内核完成真正在磁盘上进行读写的操作(创建文件描述符,通过驱动程序向硬件发送指令,将读的数据放在这个描述符对应结构体的缓冲区中,但这个结构体还是在内核区,最后还需要读到用户区)。
系统提供的read和write都是阻塞的函数。所以,应用程序在发起这样的系统调用时就必须阻塞。进程被挂起而等待文件描述符的读就绪。
重要概念:
读就绪:文件描述符的内核接收缓冲区的数据字节数大于等于其低水位标记的当前大小。
写就绪:文件描述符的内核发送缓冲区的可用空间字节数大于等于其低水位标记的当前大小。
低水位标记都是有应用程序制定,比如,应用程序指定接收低水位为64个字节,则接受缓冲区有64个字节才算fd读就绪。
完成一次I/O,向内核发起一个I/O操作,要经过等待fd就绪+内核数据到用户数据区复制。
所以,这样解释同步I/O操作 和异步I/O操作:
同步I/O操作:会导致请求进程阻塞,直到I/O操作完成。
(I/O模型:阻塞I/O、非阻塞I/O、I/O复用、信号驱动I/O)
异步I/O操作:不会导致请求进程阻塞。
(I/O模型:异步I/O)
五种I/O模型的介绍:
1、阻塞I/O:在调用recv() / recvfrom()函数时,发生在内核中等待数据和复制数据的过程。最普通的I/O模型,原生的read/write系统调用,默认是阻塞模式,导致进程阻塞。
当调用recv()函数时,系统先检查是否有准备好的数据。如果没有数据转备好,南无系统就处于等待状态。当数据准备好了以后,将数据从系统缓冲区复制到用户空间,然后返回该函数。
使用socket()函数创建的套接字默认都是阻塞的。所以当调用系统调用函数不能立即完成时,线程处于等待状态,直到操作完成。但并不是所有系统调用函数以阻塞套接字为参数时,调用都会发生阻塞:
- 输入操作:recv()、recvfrom()函数。以阻塞套接字为参数调用该函数接收数据。如果此套接字缓冲区内没有数据可读,则调用线程在数据到来之前一直睡眠。
- 输出操作:send()、sendto()函数。以阻塞套接字为参数调用该函数发送数据。如果套接字缓冲区内没有可用空间,线程会一直睡眠,知道有空间。
- 接受连接:accept()函数。以阻塞套接字为参数调用该函数,等待接受对方的连接请求。如果此时没有连接请求,线程就会进入睡眠状态。
- 外出连接:connect()函数。对于TCP连接,客户端已阻塞套接字为参数,调用该函数向服务器发起连接,该函数在收到服务器的应答前,不会返回,这意味着TCP连接总会等待至少到服务器的一次往返时间。
阻塞模式的I/O 操作适合于用户希望能够立即发送和接收数据,且处理的套接字数量比较少的情况下。
2、非阻塞I/O:通过进程反复调用I/O函数(多次系统调用,并马上返回);在数据拷贝过程中,进程是阻塞的。
这种方式通过指定系统调用read/write的参数为非阻塞,告知内核fd没有就绪时,不阻塞进程,而是返回一个错误码(一般都是WSAEWOULDBLOCK),应用进程死循环轮询(while循环体内不断调用read()函数),直到fd就绪。在这个轮询的过程中,会大量的占用CPU的时间。
在创建了套接字后,Linux下可以通过fcntl()函数将该文件描述符设置为非阻塞模式。
3、I/O复用:Linux提供select、poll、epoll。进程通过将一个或者多个fd传递给select/poll/epoll系统调用。阻塞在select/poll/epoll;这样在调用read前,select/pol/epoll可以帮我们侦测许多fd是否就绪,当有fd就绪时,就返回就绪的文件描述符个数或者就绪文件描述符队列。
I/O复用也会使进程阻塞,但与阻塞I/O相比,它的优越性在于可以实现同时对多个文件描述符进行监听,可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O进行检测,直到有数据可读或可写是=时,成才调用真正的I/O 操作函数。
4、信号驱动I/O:内核在描述符就绪时发送SIGIO信号通知进程,进程通过信号处理函数接收数据。
允许文件描述符进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作处理函数。所以,这种模式下,进程不需要等待文件描述符就绪,但依然要等待数据的拷贝。
5、异步I/O:告知内核某个操作,并让内核在整个操作(包括将数据复制到我们的进程缓冲区)完成后通知我们。这种模型和信号驱动模型的区别在于:信号驱动式I/O由内核通知我们何时可以启动一个I/O操作,而异步I/O模型由内核通知我们I/O操作何时完成。
五种I/O模型的比较: