天天看點

IO的阻塞與非阻塞、同步與異步以及Java網絡IO互動方式

摘自:http://www.cnblogs.com/zhuYears/archive/2012/09/28/2690194.html

看着感覺有點意思,挺有道理,就粘過來了;

前提

  首先先強調上下文:下面提到了同步與異步、阻塞與非阻塞的概念都是在IO的場合下。它們在其它場合下有着不同的含義,比如作業系統中,通信技術上。

  然後借鑒下《Unix網絡程式設計卷》中的理論:

  IO操作中涉及的2個主要對象為程式程序、系統核心。以讀操作為例,當一個IO讀操作發生時,通常經曆兩個步驟:

  1,等待資料準備

  2,将資料從系統核心拷貝到操作程序中

  例如,在socket上的讀操作,步驟1會等到網絡資料包到達,到達後會拷貝到系統核心的緩沖區;步驟2會将資料包從核心緩沖區拷貝到程式程序的緩沖區中。

阻塞(blocking)與非阻塞(non-blocking)IO

  IO的阻塞、非阻塞主要表現在一個IO操作過程中,如果有些操作很慢,比如讀操作時需要準備資料,那麼目前IO程序是否等待操作完成,還是得知暫時不能操作後先去做别的事情?一直等待下去,什麼事也不做直到完成,這就是阻塞。抽空做些别的事情,這是非阻塞。

  非阻塞IO會在發出IO請求後立即得到回應,即使資料包沒有準備好,也會傳回一個錯誤辨別,使得操作程序不會阻塞在那裡。操作程序會通過多次請求的方式直到資料準備好,傳回成功的辨別。

  想象一下下面兩種場景:

  A 小明和小剛兩個人都很耿直内向,一天小明來找小剛借書:“小剛啊,你那本XXX借我看看”。 于是小剛就去找書,小明就等着,找了半天找到了,把書給了小明。

  B 小明和小剛兩個人都很活潑外向,一天小明來找小剛借書:“嘿小剛,你那本XXX借我看看”。 小剛說:“我得找一會”,小明就去打球去了。過會又來,這次書找到了,把書給了小明。

  結論:A是阻塞的,B是非阻塞的。

  從CPU角度可以看出非阻塞明顯提高了CPU的使用率,程序不會一直在那等待。但是同樣也帶來了線程切換的增加。增加的 CPU 使用時間能不能補償系統的切換成本需要好好評估。

同步(synchronous)與異步(asynchronous)IO

  先來看看正式點的定義,POSIX标準将IO模型分為了兩種:同步IO和異步IO,Richard Stevens在《Unix網絡程式設計卷》中也總結道:

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

An asynchronous I/O operation does not cause the requesting process to be blocked;

  可以看出,判斷同步和異步的标準在于:一個IO操作直到完成,是否導緻程式程序的阻塞。如果阻塞就是同步的,沒有阻塞就是異步的。這裡的IO操作指的是真實的IO操作,也就是資料從核心拷貝到系統程序(讀)的過程。

  繼續前面借書的例子,異步借書是這樣的:

  C 小明很懶,一天小明來找小剛借書:“嘿小剛,你那本XXX借我看看”。 小剛說:“我得找一會”,小明就出去打球了并且讓小剛如果找到了就把書拿給他。小剛是個負責任的人,找到了書送到了小明手上。

  A和B的借書方式都是同步的,有人要問了B不是非阻塞嘛,怎麼還是同步?

  前面說了IO操作的2個步驟:準備資料和把資料從核心中拷貝到程式程序。映射到這個例子,書即是準備的資料,小剛是核心,小明是程式程序,小剛把書給小明這是拷貝資料。在B方式中,小剛找書這段時間小明的确是沒閑着,該幹嘛幹嘛,但是小剛找到書把書給小明的這個過程也就是拷貝資料這個步驟,小明還是得乖乖的回來候着小剛把書遞手上。是以這裡就阻塞了,根據上面的定義,是以是同步。

  在涉及到 IO 處理時通常都會遇到一個是同步還是異步的處理方式的選擇問題。同步能夠保證程式的可靠性,而異步可以提升程式的性能。小明自己去取書不管等着不等着遲早拿到書,指望小剛找到了送來,萬一小剛忘了或者有急事忙别的了,那書就沒了。

讨論

  說實話,網上關于同步與異步、阻塞與非阻塞的文章多之又多,大部分是拷貝的,也有些寫的非常好的。參考了許多,也借鑒了許多,也經過自己的思考。

  同步與異步、阻塞與非阻塞之間确實有很多相似的地方,很容易混淆。wiki更是把異步與非阻塞畫上了等号,更多的人還是認為他們是不同的。原因可能有很多,每個人的知識背景不同,設定的上下文也不同。

  我的看法是:在IO中,根據上面同步異步的概念,也可以看出來同步與異步往往是通過阻塞非阻塞的形式來表達的,并且是通過一種中間處理機制來達到異步的效果。同步與異步往往是IO操作請求者和回應者之間在IO實際操作階段的協作方式,而阻塞非阻塞更确切的說是一種自身狀态,目前程序或者線程的狀态。

  在發出IO讀請求後,阻塞IO會一直等待有資料可讀,當有資料可讀時,會等待資料從核心拷貝至系統程序;而非阻塞IO都會立即傳回。至于資料怎麼處理是程式程序自己的事情,無關同步和異步。 

兩種方式的組合

  組合的方式當然有四種,分别是:同步阻塞、同步非阻塞、異步阻塞、異步非阻塞。

Java網絡IO實作和IO模型

  不同的作業系統上有不同的IO模型,《Unix網絡程式設計卷》将unix上的IO模型分為5類:blocking I/O、nonblocking I/O、I/O multiplexing (select and poll)、signal driven I/O (SIGIO)以及asynchronous I/O (the POSIX aio_functions)。具體可參考《Unix網絡程式設計卷1》6.2章節。

  在windows上IO模型也是有5種:select 、WSAAsyncSelect、WSAEventSelect、Overlapped I/O 事件通知以及IOCP。具體可參考windows五種IO模型。

  Java是平台無關的語言,在不同的平台上會調用底層作業系統的不同的IO實作,下面就來說一下Java提供的網絡IO的工具和實作,為了擴大阻塞非阻塞的直覺感受,我都使用了長連接配接。

阻塞IO

  同步阻塞最常用的一種用法,使用也是最簡單的,但是 I/O 性能一般很差,CPU 大部分在空閑狀态。下面是一個簡單的基于TCP的同步阻塞的Socket服務端例子:

IO的阻塞與非阻塞、同步與異步以及Java網絡IO互動方式
1     @Test
 2     public void testJIoSocket() throws Exception
 3     {
 4         ServerSocket serverSocket = new ServerSocket(10002);
 5         Socket socket = null;
 6         try
 7         {
 8             while (true)
 9             {
10                 socket = serverSocket.accept();
11                 System.out.println("socket連接配接:" + socket.getRemoteSocketAddress().toString());
12                 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
13                 while(true)
14                 {
15                     String readLine = in.readLine();
16                     System.out.println("收到消息" + readLine);
17                     if("end".equals(readLine))
18                     {
19                         break;
20                     }
21                     //用戶端斷開連接配接
22                     socket.sendUrgentData(0xFF);
23                 }
24             }
25         }
26         catch (SocketException se)
27         {
28             System.out.println("用戶端斷開連接配接");
29         }
30         catch (IOException e)
31         {
32             e.printStackTrace();
33         }
34         finally
35         {
36             System.out.println("socket關閉:" + socket.getRemoteSocketAddress().toString());
37             socket.close();
38         }
39     }      
IO的阻塞與非阻塞、同步與異步以及Java網絡IO互動方式

  使用SocketTest作為用戶端工具進行測試,同時開啟2個用戶端連接配接Server端并發送消息,如下圖:

IO的阻塞與非阻塞、同步與異步以及Java網絡IO互動方式
IO的阻塞與非阻塞、同步與異步以及Java網絡IO互動方式

  再看下背景的列印

socket連接配接:/127.0.0.1:54080
收到消息hello!
收到消息my name is client1      

   由于伺服器端是單線程的,在第一個連接配接的用戶端阻塞了線程後,第二個用戶端必須等待第一個斷開後才能連接配接。當輸入“end”字元串斷開用戶端1,這時候看到背景繼續列印:

IO的阻塞與非阻塞、同步與異步以及Java網絡IO互動方式
socket連接配接:/127.0.0.1:54080
收到消息hello!
收到消息my name is client1
收到消息end
socket關閉:/127.0.0.1:54080
socket連接配接:/127.0.0.1:54091
收到消息hello!
收到消息my name is client2      
IO的阻塞與非阻塞、同步與異步以及Java網絡IO互動方式

   所有的用戶端連接配接在請求服務端時都會阻塞住,等待前面的完成。即使是使用短連接配接,資料在寫入 OutputStream 或者從 InputStream 讀取時都有可能會阻塞。這在大規模的通路量或者系統對性能有要求的時候是不能接受的。

阻塞IO + 每個請求建立線程/線程池

  通常解決這個問題的方法是使用多線程技術,一個用戶端一個處理線程,出現阻塞時隻是一個線程阻塞而不會影響其它線程工作;為了減少系統線程的開銷,采用線程池的辦法來減少線程建立和回收的成本,模式如下圖:

  

IO的阻塞與非阻塞、同步與異步以及Java網絡IO互動方式

  簡單的實作例子如下,使用一個線程(Accptor)接收用戶端請求,為每個用戶端建立線程進行處理(Processor),線程池的我就不弄了:

IO的阻塞與非阻塞、同步與異步以及Java網絡IO互動方式
public class MultithreadJIoSocketTest
{
    @Test
    public void testMultithreadJIoSocket() throws Exception
    {
        ServerSocket serverSocket = new ServerSocket(10002);
        Thread thread = new Thread(new Accptor(serverSocket));
        thread.start();
        
        Scanner scanner = new Scanner(System.in);
        scanner.next();
    }
    
    public class Accptor implements Runnable
    {
        private ServerSocket serverSocket;
        
        public Accptor(ServerSocket serverSocket)
        {
            this.serverSocket = serverSocket;
        }

        public void run()
        {
            while (true)
            {
                Socket socket = null;
                try
                {
                    socket = serverSocket.accept();
                    if(socket != null)
                    {
                        System.out.println("收到了socket:" + socket.getRemoteSocketAddress().toString());
                        Thread thread = new Thread(new Processor(socket));
                        thread.start();
                    }
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public class Processor implements Runnable
    {
        private Socket socket;
        
        public Processor(Socket socket)
        {
            this.socket = socket;
        }
        
        @Override
        public void run()
        {
            try
            {
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String readLine;
                while(true)
                {
                    readLine = in.readLine();
                    System.out.println("收到消息" + readLine);
                    if("end".equals(readLine))
                    {
                        break;
                    }
                    //用戶端斷開連接配接
                    socket.sendUrgentData(0xFF);
                    Thread.sleep(5000);
                }
            }
            catch (InterruptedException e)
            {
               e.printStackTrace();
            }
            catch (SocketException se)
            {
                System.out.println("用戶端斷開連接配接");
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
            finally {
                try
                {
                    socket.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
        
    }
}      
IO的阻塞與非阻塞、同步與異步以及Java網絡IO互動方式

   使用2個用戶端連接配接,這次沒有阻塞,成功的收到了2個用戶端的消息。

收到了socket:/127.0.0.1:55707
收到了socket:/127.0.0.1:55708
收到消息hello!
收到消息hello!      

   在單個線程進行中,我人為的使單個線程read後阻塞5秒,就像前面說的,出現阻塞也隻是在單個線程中,沒有影響到另一個用戶端的處理。

  這種阻塞IO的解決方案在大部分情況下是适用的,在出現NIO之前是最通常的解決方案,Tomcat裡阻塞IO的實作就是這種方式。但是如果是大量的長連接配接請求呢?不可能建立幾百萬個線程保持連接配接。再退一步,就算線程數不是問題,如果這些線程都需要通路服務端的某些競争資源,勢必需要進行同步操作,這本身就是得不償失的。