Java IO 再理解

上一次寫對Java IO的理解,主要是各種查資料進行彙總,覺得自己對IO理解的差不多了。
直到最近參加面試之後,我感覺自己對IO的理解還是有些偏差,於是重新查閱一些資料,有了一些新的見解。

同步與異步

通過上一篇博客Java IO 深入理解的介紹,我們已經知道對於一個IO操作,它會涉及到兩個系統對象,一個是調用這個IO的process(or Thread),另一個就是系統內核(Kernel)。當一個read操作發生時,它會經歷兩個階段:

  1. 等待數據準備
  2. 將數據從內核拷貝到用戶空間

這裏寫圖片描述

同步和異步的區別就是: 做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都是實現異步的一種方式,都是渣~~
具體原因在下一篇博客裏分析 高性能網絡服務器編程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章