上一次寫對Java IO的理解,主要是各種查資料進行彙總,覺得自己對IO理解的差不多了。
直到最近參加面試之後,我感覺自己對IO的理解還是有些偏差,於是重新查閱一些資料,有了一些新的見解。
同步與異步
通過上一篇博客Java IO 深入理解的介紹,我們已經知道對於一個IO操作,它會涉及到兩個系統對象,一個是調用這個IO的process(or Thread),另一個就是系統內核(Kernel)。當一個read操作發生時,它會經歷兩個階段:
- 等待數據準備
- 將數據從內核拷貝到用戶空間
同步和異步的區別就是: 做IO操作時,進程是否沒阻塞。只要有一個階段被阻塞,那就是同步。
所以對於 blocking IO(BIO),nonblocking IO(NIO)以及IO複用都是同步的,因爲無論哪一個,在數據準備完成之後,第二階段數據從內核拷貝到用戶空間都是阻塞進程的。
真正的異步是指兩個階段都不會被阻塞,當進程發起IO操作之後,就直接返回再也不關心,直到kernel發送一個信號,告訴進程說IO完成。由於IO具體完成的時間是不確定的,進程不必關心IO的結果,可以理解爲異步IO操作直接沒有依賴相關性,後續的操作不依賴這個IO,只要最終知道是否執行成功就行了~
這樣子也就更容易理解上篇博客中最後的釣魚例子了:
有A,B,C,D四個人在釣魚:
同步阻塞:A用的是最老式的魚竿,所以呢,得一直守着,等到魚上鉤了再拉桿;
同步非阻塞:B的魚竿有個功能,能夠顯示是否有魚上鉤,所以呢,B就和旁邊的MM聊天,隔會再看看有沒有魚上鉤,有的話就迅速拉桿;
IO複用:C用的魚竿和B差不多,但他想了一個好辦法,就是同時放好幾根魚竿,然後守在旁邊,一旦有顯示說魚上鉤了,它就將對應的魚竿拉起來;
異步IO: D是個有錢人,乾脆僱了一個人幫他釣魚,一旦那個人把魚釣上來了,就給D發個短信。
異步:直接把事件交給別人來辦,我接下來的操作並不依賴這個事件,只需要別人辦完之後發送短信(回調函數)告知具體情況即可!如果你需要獲得異步操作的結果,還是得在線程裏構造一個監控內存的循環進行異步讀取,直到完成。
異步必然會引起系統調用,只有系統調用才能將內核態的數據複製到用戶態
Java下AIO的實現:
異步channel API提供了兩種方式監控/控制異步操作(connect,accept, read,write等)。
1. 返回java.util.concurrent.Future對象, 檢查Future的狀態可以得到操作是否完成還是失敗,還是進行中, future.get阻塞當前進程。*這種就是上面說的,你需要獲得結果,那必然會循環獲取。
2. 爲操作提供一個回調參數java.nio.channels.CompletionHandler,這個回調類包含completed,failed兩個方法
channel的每個I/O操作都爲這兩種方式提供了相應的方法, 你可以根據自己的需要選擇合適的方式編程。
//Java AIO例子Server端 accept採用回調函數 而發送數據使用了future的方式
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.Date;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class Server {
private static Charset charset = Charset.forName("US-ASCII");
private static CharsetEncoder encoder = charset.newEncoder();
public static void main(String[] args) throws Exception {
AsynchronousChannelGroup group =
AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(4));
AsynchronousServerSocketChannel.open(group).
bind(new InetSocketAddress("0.0.0.0", 8013));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel result, Void attachment) {
server.accept(null, this); // 接受下一個連接
try {
String now = new Date().toString();
ByteBuffer buffer = encoder.encode(CharBuffer.wrap(now + "\r\n"));
//result.write(buffer, null, new CompletionHandler<Integer,Void>(){...});
//callback or
Future<Integer> f = result.write(buffer);
f.get();
System.out.println("sent to client: " + now);
result.close();
} catch (IOException | InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
}
}
//Client端實現,同樣也使用了兩種方式, connect使用了future方式,而接收數據使用了回調的方式。
public class Client {
public static void main(String[] args) throws Exception {
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
Future<Void> future = client.connect(new InetSocketAddress("127.0.0.1", 8013));
future.get();
ByteBuffer buffer = ByteBuffer.allocate(100);
client.read(buffer, null, new CompletionHandler<Integer, Void>() {
@Override
public void completed(Integer result, Void attachment) {
System.out.println("client received: " + new String(buffer.array()));
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
Thread.sleep(10000);
}
}
Linux下AIO的實現:
可以參考下面這篇博客,linux下aio異步讀寫詳解與實例。
這裏就附上其中一些的核心代碼。
其中aio_error用來查詢當前異步操作的狀態,當其狀態處於EINPROGRESS則I/O還沒完成,當處於ECANCELLED則操作已被取消,發生錯誤返回-1。
aio_write和aio_read函數類似,當該函數返回成功時,說明該寫請求以進行排隊(成功0,失敗-1)
//進行異步讀操作
//aio_write和aio_read函數類似,當該函數返回成功時,說明該寫請求以進行排隊(成功0,失敗-1)
ret = aio_read(&rd);
if(ret < 0)
{
perror("aio_read");
exit(1);
}
couter = 0;
// 循環等待異步讀操作結束
while(aio_error(&rd) == EINPROGRESS)
{
printf("第%d次\n",++couter);
}
//獲取異步讀返回值
ret = aio_return(&rd);
//異步寫操作
ret = aio_write(&wr);
if(ret < 0)
{
perror("aio_write");
}
//等待異步寫完成
while(aio_error(&wr) == EINPROGRESS)
{
printf("hello,world\n");
}
//獲得異步寫的返回值
ret = aio_return(&wr);
printf("\n\n\n返回值爲:%d\n",ret);
//AIO I/O完成時進行異步通知的代碼供理解
#define BUFFER_SIZE 1025
//AIO 操作完成回調函數
void aio_completion_handler(sigval_t sigval)
{
//用來獲取讀aiocb結構的指針
struct aiocb *prd;
int ret;
prd = (struct aiocb *)sigval.sival_ptr;
printf("hello\n");
//判斷請求是否成功
if(aio_error(prd) == 0)
{
//獲取返回值
ret = aio_return(prd);
printf("讀返回值爲:%d\n",ret);
}
}
int main(int argc,char **argv)
{
int fd,ret;
struct aiocb rd;
fd = open("test.txt",O_RDONLY);
if(fd < 0)
{
perror("test.txt");
}
//填充aiocb的基本內容
bzero(&rd,sizeof(rd));
rd.aio_fildes = fd;
rd.aio_buf = (char *)malloc(sizeof(BUFFER_SIZE + 1));
rd.aio_nbytes = BUFFER_SIZE;
rd.aio_offset = 0;
//填充aiocb中有關回調通知的結構體sigevent
rd.aio_sigevent.sigev_notify = SIGEV_THREAD;//使用線程回調通知
rd.aio_sigevent.sigev_notify_function = aio_completion_handler;//設置回調函數
rd.aio_sigevent.sigev_notify_attributes = NULL;//使用默認屬性
rd.aio_sigevent.sigev_value.sival_ptr = &rd;//在aiocb控制塊中加入自己的引用
//異步讀取文件
ret = aio_read(&rd);
if(ret < 0)
{
perror("aio_read");
}
printf("異步讀以開始\n");
//這裏sleep 是爲了等待 AIO完成,但是AIO具體的完成時間是不確定的
sleep(1);
printf("異步讀結束\n");
return 0;
}
通過分析,我們可以發現AIO好像並沒什麼卵用,確實如此!!!
但需要注意異步,不等於AIO(asynchronous IO),linux的AIO和java的AIO都是實現異步的一種方式,都是渣~~
具體原因在下一篇博客裏分析 高性能網絡服務器編程