可擴展的併發性—滿足非阻塞I / O

我們如何實現非阻塞I / O來提高應用程序的性能

可擴展的併發性—滿足非阻塞I / O

> Photo by Benjamin Voros on Unsplash

為什麼非阻塞IO更具可擴展性?

在幾乎所有現代Web應用程序中,我們都有很多I / O。 我們與數據庫對話並要求記錄或插入/更新它們。 通常,我們從硬盤訪問一些文件,這又是一個I / O操作。

我們正在討論不同的第三方Web服務,例如OAuth集成或其他功能。 如今,許多Web應用程序還以微服務的形式運行,它們必須通過HTTP請求與同一應用程序的其他部分進行對話。

如果您使用Ruby,Python或許多其他語言編寫Web應用程序,則默認情況下所有這些與I / O相關的任務都處於阻塞狀態,這意味著該過程將等待直到收到響應,然後繼續執行程序。

另一方面,Node.js [1]默認情況下正在使用非阻塞I / O。 因此,該過程可以繼續在其他地方工作,並在請求完成時執行回調或promise。

這使操作系統可以充分利用一個CPU內核。 但是,其他編程語言也可以使用非阻塞編程模型嗎?

是的! 在此博客文章中,我們將討論如何使用(幾乎)非阻塞I / O在Ruby中編寫本機事件循環,然後瞭解如何改進此設計。


簡單實現

首先,讓我們看一個可行的本機實現:

<code>

def

process_partial_file_input

(data)

end

big_file =

"network/path/big_file.xlsx"

file_handler = open(big_file) files_to_look_after = [file_handler] loop

do

puts

"(Re)Starting the Event loop"

readable, _writeable = IO.select files_to_look_after, [], [],

0

.

01

if

readable readable.each

do

|ready_io|

read_data = ready_io.read_nonblock(

4096

) process_partial_file_input read_data

if

read_data

rescue

EOFError => e files_to_look_after.reject! {

|file|

file == ready_io }

end

end

break

if

files_to_look_after.empty?

end

/<code>

在討論如何改進此設計之前,讓我們簡短地討論IO.select方法,因為這是事件循環的核心。

IO.select

如評論中所述,此方法是跨平臺的,可在運行程序的任何地方使用。

它採用的第一個參數是程序要讀取的I / O描述符數組(文件描述符,Unix套接字或類似的東西)。

第二個數組仍然是I / O描述符的數組,但這一次它用於可寫連接。

第三個數組是錯誤數組。

最後,最後一個參數是超時。 這是該方法阻塞的最長時間。 因此,在上面的示例中,我們可以說一個刻度至少為10毫秒,這取決於數據處理所花費的時間。

簡單的事件循環的設計討論

當我們看一下這段代碼時,缺點很明顯。 併發引入的複雜性與業務邏輯糾纏在一起,並且分離很困難。

事件循環知道我們的業務邏輯,因為它立即調用了該方法。 我們可以藉助可處理所有讀/寫事件的寄存器來改善此情況。

寄存器可以利用帶有兩個鍵的簡單哈希來進行讀寫,然後在其中保存回調。 在Ruby中,回調可以是任何塊,proc或lambda。 同樣,一個簡單的實現可能看起來像這樣:

<code>

class

CallbackRegister

def

initialize

@callbacks = {

read:

[],

write:

[]}

end

def

each

(type, &block)

@callbacks[type].each

do

|callback|

yield

callback

end

end

def

push

(callback, type)

@callbacks[type] << callback

end

end

big_file =

"network/path/big_file.xlsx"

file_handler = open(big_file) files_to_look_after = [file_handler] register = get_callback_register_from_container_manager loop

do

puts

"(Re)Starting the Event loop"

readable, _writeable = IO.select files_to_look_after, [], [],

0

.

01

if

readable readable.each

do

|ready_io|

read_data = ready_io.read_nonblock(

4096

) register.each(

:read

)

do

|callback|

callback.call(ready_io, read_data)

end

rescue

EOFError => e files_to_look_after.reject! {

|file|

file == ready_io }

end

end

break

if

files_to_look_after.empty?

end

/<code>

現在,我們已將業務邏輯與併發邏輯分離。 但這仍然會導致回調地獄。

JavaScript曾經有很多這個問題,但是它通過promise以及最近的async await功能解決了這個問題。 這樣,您可以編寫可同時運行的順序代碼。

儘管如此,我們在此設計中還有其他缺點。 它仍然使用一組固定的描述符來照料,並且我們沒有地方在運行時進行配置。 此外,儘管我們可能不希望這樣做,但每個回調事件都會收到通知,通知每個回調事件。

我們該如何改善? 符合反應堆模式。

反應堆模式

反應器模式是大多數事件循環的基礎。 它將應用程序邏輯與切換實現完全分開,因此使代碼更易於維護和重用。

它由兩個主要部分組成:一個事件多路複用器和一個調度程序,並與另外兩個一起工作-資源和請求處理程序。

反應器使用單線程事件循環,在事件多路複用器中註冊資源,並在事件觸發後分派給回調。

從我們的示例中可以看出,這種方式不需要阻塞I / O,因此進程可以最大限度地利用CPU內核。

實作

Ruby中著名的實現是EventMachine,Celluloid和async。 Python也至少有一個很好的實現,即Twisted。 PHP具有ReactPHP,我可以肯定幾乎所有其他語言也都具有不錯的實現。

缺點

與其他所有內容一樣,反應堆也有一些缺點,您必須意識到這些缺點,才能做出明智的決定,即使用這種模式是否對您的用例有意義。

主要的缺點是,如果其中一個貪婪並且將花費大量時間直到完成,它將阻止所有回調。

本質上,反應堆是一種協作併發。 如上所述,反應器是單線程的,如果從一個回調中充分利用了CPU,則其他所有操作都必須等待。

另一個限制是,由於邏輯流程不是程序運行的方式,因此難以調試反應堆模式。 這也給開發人員帶來了更多的麻煩。

從這裡開始

對於併發I / O,反應堆模式是最好的選擇嗎?

實際上,不,仍然有一些方法可以對此進行改進。 如上所述,傳統的反應器使用多路分解器同步調度事件,並且必須等待回調完成。 我們也可以使用前攝器模式使此異步。

如果您仍然需要更高的性能,那就扔硬件吧! 在某些時候,這是您最好的選擇。 而且,如果您需要執行此操作,那麼微服務體系結構將派上用場,因為您可以獨立擴展應用程序的一小部分。

[1] Node.js只是一個例子,因為這是最常用的平臺,默認使用非阻塞I / O。

(本文翻譯自Gernot Gradwohl的文章《Scalable Concurrency — Meet Non-Blocking I/O》,參考:
https://medium.com/better-programming/scalable-concurrency-meet-non-blocking-i-o-edb6b39c59d7)


分享到:


相關文章: