偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

IO 任務(wù)與 CPU 調(diào)度藝術(shù)

系統(tǒng)
本文針對幾個話題對一些操作系統(tǒng)下關(guān)于性能指標評估的話題進行深入分析。

近期和同行談及一些操作系統(tǒng)下關(guān)于性能指標評估的話題,涉及一些計算機基礎(chǔ)的核心知識點,遂以此文針對如下幾個話題進行深入分析:

  • 為什么并發(fā)的IO任務(wù)使用多線程效率更高?
  • CPU在任務(wù)IO阻塞時發(fā)生了什么?
  • CPU切換線程的依據(jù)是什么?
  • 線程休眠有什么用?
  • 線程休眠1秒后是否會立刻拿到CPU執(zhí)行權(quán)。
  • 為什么有人代碼會用到Thread.sleep(0);它的作用是什么?

一、詳解操作系統(tǒng)對于線程的調(diào)度

1. 操作系統(tǒng)的任務(wù)調(diào)度

近代CPU運算速度是非??斓模词乖诙嗑€程情況下CPU會按照操作系統(tǒng)的調(diào)度算法有序快速有序的執(zhí)行任務(wù),使得我們即使開個十幾個進程,肉眼上所有的進程幾乎的是并行運行的,這本質(zhì)上就是CPU對應(yīng)ns級別的線程任務(wù)調(diào)度切換以及人眼200ms下不會直觀感受到停頓的共同作用:

CPU執(zhí)行線程時會按照任務(wù)優(yōu)先級進行處理,一般而言,對于硬件產(chǎn)生的信號優(yōu)先級都是最高的,當收到中斷信號時,CPU理應(yīng)中斷手頭的任務(wù)去處理硬件中斷程序。例如:用戶鍵盤打字輸入、收取網(wǎng)絡(luò)數(shù)據(jù)包。

以用戶鍵盤打字輸入為例,從鍵盤輸入到CPU處理的流程為:

  • 用戶在鍵盤鍵入一個按鍵指令
  • 鍵盤給CPU發(fā)送一個中斷引腳發(fā)送一個高電平。
  • CPU執(zhí)行鍵盤的中斷程序,獲取鍵盤的數(shù)據(jù)。

同理,獲取網(wǎng)絡(luò)數(shù)據(jù)包的執(zhí)行流程為:

  • 網(wǎng)卡收到網(wǎng)線傳輸?shù)木W(wǎng)絡(luò)數(shù)據(jù)
  • 通過硬件電路完成數(shù)據(jù)傳輸
  • 將數(shù)據(jù)寫入到內(nèi)存中的某個地址中
  • 網(wǎng)卡發(fā)送一個中斷信號給CPU
  • CPU響應(yīng)網(wǎng)卡中斷程序,從內(nèi)存中讀取數(shù)據(jù)

了解網(wǎng)絡(luò)數(shù)據(jù)包獲取流程整個流程后,不知道讀者是否發(fā)現(xiàn),網(wǎng)卡讀取數(shù)據(jù)期間CPU似乎無需參與工作的,那么操作系統(tǒng)是如何處理這期間的任務(wù)調(diào)度呢?

2. IO阻塞的線程會如何避免CPU資源占用

操作系統(tǒng)為了支持多任務(wù),將任務(wù)分為了運行、等待、就緒等幾種狀態(tài),對于運行狀態(tài)的任務(wù),操作系統(tǒng)會將其放到工作隊列中。CPU按照操作系統(tǒng)的調(diào)度算法按需執(zhí)行工作隊列中的任務(wù)。

需要注意的是,這些任務(wù)能夠被CPU時間片完整執(zhí)行的前提是任務(wù)不會發(fā)生阻塞。一旦任務(wù)或是讀取本地文件或者發(fā)起網(wǎng)絡(luò)IO等原因發(fā)起阻塞,這些線程任務(wù)就會被放到等待隊列中,就下圖所有的收取網(wǎng)絡(luò)數(shù)據(jù)包,在網(wǎng)卡讀取數(shù)據(jù)并寫入到內(nèi)存這期間,該任務(wù)就是在等待隊列中完成的。 只有這些IO任務(wù)接受到了完整的數(shù)據(jù)并通過中斷程序發(fā)送信號給CPU,操作系統(tǒng)才會將其放到工作隊列中,讓CPU讀取數(shù)據(jù)。

這也就是IO阻塞避免CPU資源消耗的原因,即在IO阻塞態(tài)時,CPU會將這些任務(wù)掛起切換執(zhí)行其它任務(wù),等其IO數(shù)據(jù)準備就緒并發(fā)起中斷信號時,再回頭處理這些任務(wù)。

3. 用一個實例了解網(wǎng)絡(luò)收包的過程

對于上述問題,我們不妨看一段這樣的代碼,功能很簡單,服務(wù)端開啟9009端口獲取客戶端輸入的信息。

服務(wù)端代碼如下,邏輯也很清晰,執(zhí)行步驟為:

  • 創(chuàng)建ServerSocket 服務(wù)器。
  • 綁定端口。
  • 阻塞監(jiān)聽等待客戶端連接。
  • 處理客戶端發(fā)送的數(shù)據(jù)。
  • 回復數(shù)據(jù)給客戶端。
public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = null;

        try {
            // 創(chuàng)建服務(wù)器 Socket 并綁定 9009 端口
            serverSocket = new ServerSocket(9009);
        } catch (IOException e) {
            System.err.println("Could not listen on port: 9009.");
            System.exit(1);
        }

        Socket clientSocket = null;
        System.out.println("Waiting for connection...");

        try {
            // 等待客戶端連接
            clientSocket = serverSocket.accept();
            System.out.println("Connection successful!");
        } catch (IOException e) {
            System.err.println("Accept failed.");
            System.exit(1);
        }

        //輸出流
        PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);

        //輸入流
        BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

        String inputLine;

        while ((inputLine = in.readLine()) != null) { // 不斷讀取客戶端發(fā)送的消息
            System.out.println("Client: " + inputLine);
            out.println("Server: Welcome to the server!"); // 向客戶端發(fā)送歡迎消息
        }

        out.close();
        in.close();
        clientSocket.close();
        serverSocket.close();
    }
}

客戶端代碼示例如下,執(zhí)行步驟為:

  • 連接服務(wù)端。
  • 輸入要發(fā)送的數(shù)據(jù)。
  • 發(fā)送數(shù)據(jù)。
  • 獲取響應(yīng)。
public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = null;
        PrintWriter out = null;
        BufferedReader in = null;

        try {
            socket = new Socket("localhost", 8080); // 連接到服務(wù)器
            out = new PrintWriter(socket.getOutputStream(), true);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        } catch (UnknownHostException e) {
            System.err.println("Unknown host: localhost.");
            System.exit(1);
        } catch (IOException e) {
            System.err.println("Couldn't get I/O for the connection to: localhost.");
            System.exit(1);
        }

        BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
        String userInput;

        while ((userInput = stdIn.readLine()) != null) { // 不斷從控制臺讀取用戶輸入
            out.println(userInput); // 向服務(wù)器發(fā)送消息
            System.out.println("Server: " + in.readLine()); // 從服務(wù)器讀取消息并打印到控制臺
        }

        out.close();
        in.close();
        stdIn.close();
        socket.close();
    }
}

啟動服務(wù)端,我們會看到這樣一段輸出:

Waiting for connection...

并通過客戶端發(fā)送字符串hello world,服務(wù)端的輸出結(jié)果如下:

Waiting for connection...
Connection successful!
Client: hello world

了解整個流程之后,我們再對細節(jié)進行分析。對于服務(wù)端的每一個步驟,CPU對應(yīng)做法如下:

(1) new ServerSocket(9009) 新建由文件系統(tǒng)管理的Socket對象,并綁定9009端口。

(2) serverSocket.accept();阻塞監(jiān)聽等待客戶端連接,此時CPU就會將其放到等待隊列中,去處理其他線程任務(wù)。

(3) 客戶端發(fā)起連接后,服務(wù)端網(wǎng)卡收到客戶端請求連接,通過中斷程序發(fā)出信號,CPU收到中斷信號后掛起當前執(zhí)行的線程去響應(yīng)連接請求。

(4) 服務(wù)端建立連接成功,輸出Connection successful!

(5) in.readLine()阻塞獲取用戶發(fā)送數(shù)據(jù),CPU再次將其放到等待隊列中,處理其他非阻塞的線程任務(wù)。

(6) 客戶端發(fā)送數(shù)據(jù),網(wǎng)卡接收并將其存放到內(nèi)存中,通過中斷程序發(fā)出信號,CPU收到中斷信號后掛起當前執(zhí)行的線程去讀取響應(yīng)數(shù)據(jù)。

(7) 重復5、6兩步。

二、CPU如何處理任務(wù)優(yōu)先級分配

上文我們提到過CPU會按照某種調(diào)度算法執(zhí)行進程任務(wù),這里的算法大致分為兩種:

  • 搶占式
  • 非搶占式

先來說說搶占式算法,典型實現(xiàn)就是Windows系統(tǒng),它會在調(diào)度前計算每一個線程的優(yōu)先級,然后按照優(yōu)先級執(zhí)行任務(wù),執(zhí)行任務(wù)直到執(zhí)行到線程主動掛起釋放執(zhí)行權(quán)或者CPU察覺到該線程霸占CPU執(zhí)行時間過長將其強行掛起。 此后會再次重新計算一次優(yōu)先級,在這期間,那些等待很久的線程優(yōu)先級就會被大大提高,然后CPU再次找出優(yōu)先級最高的線程任務(wù)執(zhí)行。 之所以我們稱這種算法為搶占式,是因為每次進行重新分配時不一定是公平的。假設(shè)線程1第一次執(zhí)行到期后,CPU重新計算優(yōu)先級,結(jié)果發(fā)現(xiàn)還是線程1優(yōu)先級最高,那么線程1依然會再次獲得CPU執(zhí)行權(quán),這就導致其他線程一直沒有執(zhí)行的機會,極可能出現(xiàn)線程饑餓的情況。

Unix操作系統(tǒng)用的就是非搶占式調(diào)度算法,即時間分片算法,它會將時間平均切片,每一個進程都會得到一個平均的執(zhí)行時間,只有任務(wù)執(zhí)行完分片算法分配的時間或者在執(zhí)行期間發(fā)生阻塞,CPU才會切換到下一個線程執(zhí)行。因為時間分片是平均的,所以分片算法可以保證盡可能的公平。

三、詳解Java中的阻塞方法Thread.sleep()

1. Thread.sleep()如何優(yōu)化搶占式調(diào)度的饑餓問題

上文提到搶占式算法可能導致線程饑餓的問題,所以我們是否有什么辦法讓長時間霸占CPU的線程主動讓CPU重新計算一次優(yōu)先級呢? 答案就是Thread.sleep()方法,通過該方法就相當于對當前線程任務(wù)的一次洗牌,它會讓當前線程休眠進入等待隊列,此時CPU就會重新計算任務(wù)優(yōu)先級。這樣一來那些因為長時間等待使得優(yōu)先級被拔高的線程就會被CPU優(yōu)先處理了:

2. RocketMQ中關(guān)于Thread.sleep(0)的經(jīng)典案例

對應(yīng)代碼如下可以看到在RocketMQ這個大循環(huán)中,處理一些刷盤的操作,該因為是大循環(huán),且涉及數(shù)據(jù)來回傳輸?shù)炔僮鳎匝h(huán)期間勢必會創(chuàng)建大量的垃圾對象。

所以代碼中有個if判斷調(diào)用了Thread.sleep(0),作用如上所說,假設(shè)運行Java程序的操作系統(tǒng)采用搶占式調(diào)度算法,可能會出現(xiàn)以下流程:

  • 大循環(huán)長時間霸占CPU導致處理GC任務(wù)的線程遲遲無法工作。
  • 循環(huán)結(jié)束后堆內(nèi)存中出現(xiàn)大量因為刷盤等業(yè)務(wù)操作留下的垃圾對象。
  • 等待長時間后,操作系統(tǒng)重新進行一次CPU競爭,假設(shè)此時等待已久的處理GC任務(wù)的線程優(yōu)先級最高,于是執(zhí)行權(quán)分配給了GC線程。
  • 因為堆內(nèi)存垃圾太多,導致長時間的GC。

所以設(shè)計者們考慮到這一點,這在循環(huán)內(nèi)部每一個小節(jié)點時調(diào)用Thread.sleep(),確保每執(zhí)行一小段時間執(zhí)行讓操作系統(tǒng)進行一次CPU競爭,讓GC線程盡可能多執(zhí)行,做到垃圾回收的削峰填谷,避免后續(xù)出現(xiàn)一次長時間的GC時間導致STW進而阻塞業(yè)務(wù)線程的運行。

for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
        byteBuffer.put(i, (byte) 0);
        // force flush when flush disk type is sync
        if (type == FlushDiskType.SYNC_FLUSH) {
            if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
                flush = i;
                mappedByteBuffer.force();
            }
        }

        // prevent gc
        if (j % 1000 == 0) {
            log.info("j={}, costTime={}", j, System.currentTimeMillis() - time);
            time = System.currentTimeMillis();
            try {
                Thread.sleep(0);
            } catch (InterruptedException e) {
                log.error("Interrupted", e);
            }
        }
 }

那為什么設(shè)計者們不使用Thread.sleep()而是調(diào)用Thread.sleep(0)方法呢?原因如下:

  • 調(diào)用sleep方法僅僅是為了讓操作系統(tǒng)重新進行一次CPU競爭,并不是為了掛起當前線程。
  • 并不是每次sleep都需要垃圾回收,設(shè)置為0可以確保當前大循環(huán)的線程讓出CPU執(zhí)行權(quán)并休眠0s,即一讓出CPU時間片就參與CPU下一次執(zhí)行權(quán)的競爭。

不得不說RocketMQ的設(shè)計們對于編碼的功力是非常深厚的。

四、小結(jié)

到此為止,我們了解的操作系統(tǒng)對于CPU執(zhí)行線程任務(wù)的調(diào)度流程,回到我們文章開頭提出的幾個問題:

(1) 為什么并發(fā)的IO任務(wù)使用多線程效率更高?

答:IO阻塞的任務(wù)會讓出CPU時間片,自行處理IO請求,確保操作系統(tǒng)盡可能榨取CPU利用率。

(2) CPU在任務(wù)IO阻塞時發(fā)生了什么?

答:將任務(wù)放入等待隊列,并切換到下一個要執(zhí)行的線程中。

(3) CPU切換線程的依據(jù)是什么?

答:有可能是分配給線程的時間片到期了,有可能是因為線程阻塞,還有可能因為線程霸占CPU太久了(針對搶占式算法)

(4) 線程休眠有什么用?

答:以搶占式算法為例,線程休眠會將當前任務(wù)存入等待隊列,并讓CPU重新計算任務(wù)優(yōu)先級,選出當前最高優(yōu)先級的任務(wù)。

(5) 線程休眠1秒后是否會立刻拿到CPU執(zhí)行權(quán)。

答:不一定,CPU會按照調(diào)度算法執(zhí)行任務(wù),這個不能一概而論。

(6) 為什么有人代碼會用到`Thread.sleep(0);`它的作用是什么?

答:讓當前線程讓出CPU執(zhí)行權(quán),所有線程重新進行一次CPU競爭,優(yōu)先級高的獲取CPU執(zhí)行權(quán)。

責任編輯:趙寧寧 來源: 寫代碼的SharkChili
相關(guān)推薦

2019-09-17 14:31:37

磁盤排序IO

2019-06-29 14:34:27

磁盤IO排序

2023-05-08 16:38:46

任務(wù)調(diào)度分布式任務(wù)調(diào)度

2024-10-25 09:26:56

2022-09-25 21:45:54

日志平臺

2021-05-13 12:00:51

cron調(diào)度任務(wù)系統(tǒng)運維

2023-12-26 07:44:00

Spring定時調(diào)度

2020-04-01 16:10:02

PythonAPScheduler調(diào)度

2013-12-17 10:15:19

OpenMP任務(wù)調(diào)度

2023-11-16 09:30:27

系統(tǒng)任務(wù)

2021-05-20 09:50:20

鴻蒙HarmonyOS應(yīng)用

2025-05-06 00:00:00

CPU調(diào)度算法

2009-06-19 15:20:08

Quartz任務(wù)調(diào)度Spring

2020-09-29 19:20:05

鴻蒙

2023-06-26 00:14:28

Openjob分布式任務(wù)

2022-09-16 11:23:59

Python框架Celery

2023-09-25 08:55:15

CSS設(shè)計軟件

2022-09-21 12:01:22

消息隊列任務(wù)隊列任務(wù)調(diào)度

2017-10-30 15:14:45

盤纖光纖光纜

2020-11-06 12:12:35

HarmonyOS
點贊
收藏

51CTO技術(shù)棧公眾號