實用技巧:高併發應用程序的設計原理和模式

1.概述

在本文中,咱們將討論爲創建高度併發的應用程序而逐漸創建的一些設計原理和模式。nginx

可是,值得注意的是,設計併發應用程序是一個普遍而複雜的主題,所以,沒有任何教程能夠聲稱對它的處理是詳盡無遺的。咱們這裏將介紹一些經常使用的技巧!算法

2.併發基礎

在繼續進行以前,先來了解基礎知識。首先,咱們必須闡明咱們對所謂的併發程序的理解:若是同時進行多個計算,則咱們將程序稱爲併發程序。數據庫

如今,請注意,咱們已經提到了計算是同時發生的,也就是說,它們是同時進行的。可是,它們可能同時執行或不一樣時執行,瞭解差別是很重要的,由於同時執行的計算稱爲parallel編程

2.1 如何建立併發模塊?

瞭解咱們如何建立併發模塊很重要。有不少選項,但咱們將在此處重點介紹兩個流行的選擇:設計模式

  • 進程:進程是正在運行的程序的實例,該程序與同一計算機中的其餘進程隔離。機器上的每一個進程都有本身獨立的時間和空間。所以,一般不可能在進程之間共享內存,而且它們必須經過傳遞消息進行通訊。
  • 線程:另外一方面,線程只是進程的一部分,一個程序中能夠有多個線程共享同一內存空間。可是,每一個線程都有惟一的堆棧和優先級。線程能夠是本地線程(由操做系統本地調度)或綠色(由運行時庫調度)。

2.2 併發模塊如何交互?

若是併發模塊沒必要通訊,這是很是理想的,可是一般不是這種狀況。這產生了兩種併發編程模型:瀏覽器

  • 共享內存:在此模型中,併發模塊經過在內存中讀寫共享對象進行交互。這一般致使併發計算的交錯,從而致使競爭條件。所以,它能夠不肯定地致使不正確的狀態。
【實用技巧篇】高併發應用程序的設計原理和模式

  • 消息傳遞:在此模型中,併發模塊經過經過通訊通道相互傳遞消息來進行交互。在這裏,每一個模塊順序處理傳入的消息。因爲沒有共享狀態,所以編程相對容易一些,但這仍然沒法擺脫競爭條件!
【實用技巧篇】高併發應用程序的設計原理和模式

2.3 併發模塊如何執行?

自從摩爾定律在處理器的時鐘速度方面碰壁以來已經有一段時間了。取而代之的是,因爲必須增加,所以咱們開始將多個處理器打包到同一芯片上,一般稱爲多核處理器。可是,聽到具備32個以上內核的處理器並不罕見。緩存

如今,咱們知道單個內核一次只能執行一個線程或一組指令。可是,進程和線程的數量能夠分別爲數百和數千。那麼,它如何真正起做用?這是操做系統爲咱們模擬併發的地方。操做系統經過時間分片來實現這一點-這實際上意味着處理器會頻繁地,不可預測地且不肯定地在線程之間切換。安全

3.並行編程中的問題

在很大程度上,咱們在並行編程方面的經驗涉及將本機線程與共享內存一塊兒使用。所以,咱們將專一於由此產生的一些常見問題:bash

  • 互斥(同步基元):交織線程須要對共享狀態或內存具備獨佔訪問權限,以確保程序的正確性。共享資源的同步是一種實現互斥的流行方法。有幾種同步原語可供使用-例如,鎖,監視器,信號燈或互斥鎖。可是,互斥編程很容易出錯,一般會致使性能瓶頸。與死鎖和活鎖相關的問題有不少討論的。
  • 上下文切換(重量級線程):每一個操做系統都有本機(儘管有所不一樣)對進程和線程等併發模塊的支持。如討論的那樣,操做系統提供的基本服務之一是調度線程,以便經過時間分片在有限數量的處理器上執行。如今,這實際上意味着線程常常在不一樣狀態之間切換。在此過程當中,須要保存並恢復其當前狀態。這是一項耗時的活動,直接影響總體吞吐量。

4.高併發設計模式

如今,咱們瞭解了併發編程的基礎知識以及其中的常見問題,是時候瞭解一些避免這些問題的常見模式了。咱們必須重申,併發編程是一項艱鉅的任務,須要大量經驗。所以,遵循某些已創建的模式可使任務更容易。服務器

4.1 基於Actor的併發

咱們將針對併發編程討論的第一個設計稱爲Actor模型。這是並行計算的數學模型,基本上將一切都視爲參與者。參與者能夠相互傳遞消息,而且響應消息能夠作出本地決策。這是由卡爾·休伊特(Carl Hewitt)首次提出的,並啓發了許多編程語言。

Scala用於併發編程的主要構造是參與者。Actor是Scala中的普通對象,咱們能夠經過實例化Actor類來建立。此外,Scala Actors庫提供了許多有用的actor操做:

class myActor extends Actor {    def act() {        while(true) {            receive {                // Perform some action            }        }    }}複製代碼

在上面的示例中,在無限循環內對receive方法的調用將使actor掛起,直到消息到達爲止。到達後,郵件將從參與者的郵箱中刪除,並採起了必要的措施。

【實用技巧篇】高併發應用程序的設計原理和模式

actor模型消除了併發編程的基本問題之一-------共享內存。參與者經過消息進行通訊,而且每一個參與者依次處理其專用郵箱中的消息。可是,咱們經過線程池執行角色,並且咱們已經看到,本地線程多是重量級的,所以數量有限。

固然,這裏還有其餘模式能夠爲咱們提供幫助,稍後將介紹這些模式!

4.2 基於事件的併發

基於事件的設計明確解決了本機線程生成和操做成本高昂的問題。基於事件的設計之一是事件循環。事件循環與事件提供程序和一組事件處理程序一塊兒使用。在這種設置中,事件循環在事件提供程序上阻塞,並在到達時將事件調度到事件處理程序

基本上,事件循環不過是事件分配器!事件循環自己能夠僅在單個本機線程上運行。那麼,事件循環中到底發生了什麼?讓咱們來看一個很是簡單的事件循環的僞代碼做爲示例:

while(true) {    events = getEvents();    for(e in events)        processEvent(e);}複製代碼

基本上,咱們的事件循環所要作的就是不斷尋找事件,並在發現事件後對其進行處理。該方法確實很簡單,但能夠從事件驅動的設計中受益。

使用此設計構建併發應用程序可爲應用程序提供更多控制。並且,它消除了多線程應用程序的一些典型問題,例如死鎖。

【實用技巧篇】高併發應用程序的設計原理和模式

JavaScript實現事件循環以提供異步編程。它維護一個調用堆棧以跟蹤要執行的全部功能。它還維護一個事件隊列,用於發送新功能進行處理。事件循環不斷檢查調用堆棧,並從事件隊列中添加新功能。全部異步調用都會分派到一般由瀏覽器提供的Web API。

事件循環自己能夠在單個線程上運行,可是Web API提供了單獨的線程。

4.3 非阻塞算法

在非阻塞算法中,一個線程的掛起不會致使其餘線程的掛起。咱們已經看到,咱們的應用程序中只能有數量有限的本機線程。如今,阻塞在線程上的算法明顯下降了吞吐量, 並阻止了咱們構建高度併發的應用程序。

非阻塞算法始終使用底層硬件提供的比較交換原子原語。這意味着硬件將比較存儲位置的內容與給定值,而且只有它們相同時,纔會將值更新爲新的給定值。這看起來很簡單,但實際上爲咱們提供了一個原子操做,不然將須要同步。

這意味着咱們必須編寫使用此原子操做的新數據結構和庫。這爲咱們提供了多種語言的大量免等待和免鎖實現。Java具備幾種非阻塞數據結構,例如AtomicBoolean,AtomicInteger,AtomicLong和AtomicReference。

考慮一個有多個線程試圖訪問相同代碼的應用程序:

boolean open = false;if(!open) {    // Do Something    open=false;}複製代碼

顯然,上面的代碼不是線程安全的,而且它在多線程環境中的行爲多是不可預測的。咱們的選擇是將這段代碼與鎖同步,或者使用原子操做:

AtomicBoolean open = new AtomicBoolean(false);if(open.compareAndSet(false, true) {    // Do Something}複製代碼

如咱們所見,使用像AtomicBoolean這樣的非阻塞數據結構能夠幫助咱們編寫線程安全的代碼,而不會沉迷於鎖的弊端!

5.支持編程語言

咱們已經看到,能夠經過多種方式構造併發模塊。儘管編程語言確實有所做爲,但主要是底層操做系統如何支持該概念。可是,因爲本機線程支持的基於線程的併發在可伸縮性方面遇到了新的障礙,所以咱們始終須要新的選擇。

咱們可使用的一種解決方案是綠色線程。綠色線程是由運行時庫調度的線程,而不是由底層操做系統本地調度的線程。儘管這並不能解決基於線程的併發中的全部問題,但在某些狀況下,它確定能夠爲咱們提供更好的性能。

如今,除非咱們選擇使用的編程語言支持綠色線程,不然使用綠色線程並不是易事。並不是每種編程語言都具備此內置支持。一樣,咱們能夠經過不一樣的編程語言以很是獨特的方式來實現咱們所謂的綠色線程。讓咱們來看一些可用的選項。

5.1 Go中的Goroutines

Go編程語言中的Goroutine 是輕量級線程。它們提供能夠與其餘功能或方法同時運行的功能或方法。Goroutines 很是便宜,由於它們從開始只佔用幾千字節的堆棧大小

最重要的是,goroutines與較少數量的本機線程複用。此外,goroutine使用通道相互通訊,從而避免了對共享內存的訪問。咱們幾乎得到了所需的一切,而後猜想-什麼都不作!

5.2 Erlang中的進程

在Erlang中,每一個執行線程稱爲一個進程。可是,這與咱們到目前爲止討論的過程不太同樣!Erlang進程重量輕,內存佔用少,而且建立和處理速度快,調度開銷低。

在幕後,Erlang進程不過是運行時爲之調度的功能。並且,Erlang進程不共享任何數據,它們經過消息傳遞相互通訊。這就是爲何咱們首先稱這些「過程」的緣由!

5.3 Java中的Fiber(提案)

Java併發的故事一直在不斷髮展。Java確實從一開始就對綠色線程(至少對Solaris操做系統)提供了支持。可是,因爲障礙超出了本教程的範圍,所以已中止使用。

從那時起,Java中的併發所有與本地線程有關,以及如何巧妙地使用它們!可是出於顯而易見的緣由,咱們可能很快就會在Java中有了一個新的併發抽象,稱爲光纖。Project Loom建議將延續與纖維一塊兒引入,這可能會改變咱們用 Java 編寫併發應用程序的方式

這只是對不一樣編程語言中可用功能的簡要介紹。其餘編程語言還嘗試了更多有趣的方式來處理併發。

此外,值得注意的是,在設計高度併發的應用程序時,上一節中討論的設計模式與編程語言對相似綠線程的抽象的支持的組合可能會很是強大。

6.高併發應用

實際應用程序一般具備多個組件,這些組件經過導線相互交互。咱們一般經過Internet對其進行訪問,它包含多種服務,例如代理服務,網關,Web服務,數據庫,目錄服務和文件系統。

在這種狀況下,咱們如何確保高併發性?讓咱們探索其中的一些層以及構建高度併發應用程序所具備的選項。

構建高併發應用程序的關鍵是使用此處討論的一些設計概念。咱們須要爲工做選擇合適的軟件-----已經結合了其中一些實踐的軟件。

6.1 網頁層

Web一般是用戶請求到達的第一層,所以在此處不可避免地須要進行高併發性設置。讓咱們看看其中的一些選項:

  • Node(也稱爲NodeJS或Node.js)是基於Chrome的V8 JavaScript引擎構建的開源,跨平臺JavaScript運行時。在處理異步I / O操做時,Node工做得很好。Node之因此如此出色,是由於它在單個線程上實現了一個事件循環。藉助回調的事件循環可異步處理全部阻塞操做,例如I / O。
  • nginx一個開放源代碼的Web服務器,除其餘用法外,咱們一般將用做反向代理。nginx提供高併發性的緣由是它使用了異步的,事件驅動的方法。nginx在一個線程中與主進程一塊兒運行。主流程維護執行實際處理的工做流程。所以,工做進程同時處理每一個請求。

6.2 應用層

在設計應用程序時,有多種工具可幫助咱們構建高併發性,先檢查一下其中一些可用的庫和框架:

  • Akka用Scala編寫的工具包,用於在JVM上構建高度併發和分佈式的應用程序。Akka處理併發的方法基於咱們前面討論的參與者模型。Akka在參與者和基礎系統之間建立了一層。該框架處理建立和調度線程,接收和調度消息的複雜性。
  • Project Reactor一個反應式庫,用於在JVM上構建非阻塞應用程序。它基於Reactive Streams規範,專一於有效的消息傳遞和需求管理(背壓)。反應堆操做員和調度程序能夠維持較高的消息吞吐率。幾個流行的框架提供了Reactor的實現,包括Spring WebFlux和RSocket。
  • Netty是一個異步的,事件驅動的網絡應用程序框架。咱們可使用Netty開發高度併發的協議服務器和客戶端。Netty利用了 NIO,它是Java API的集合,可經過緩衝區和通道提供異步數據傳輸。它爲咱們提供了多個優點,例如更好的吞吐量,更低的延遲,更少的資源消耗以及最小化沒必要要的內存複製

6.3 資料層

沒有數據就沒有完整的應用程序,數據來自持久性存儲。當咱們討論有關數據庫的高併發性時,大多數重點仍放在NoSQL系列上。這主要是因爲NoSQL數據庫能夠提供線性可伸縮性,可是在關係型變量中卻很難實現。讓咱們看一下數據層的兩個流行工具:

  • Cassandra是一個免費的開源NoSQL分佈式數據庫,可在商品硬件上提供高可用性,高可伸縮性和容錯能力。可是,Cassandra不提供跨越多個表的ACID事務。所以,若是咱們的應用程序不須要強一致性和事務性,那麼咱們能夠受益於Cassandra的低延遲操做。
  • Kafka是一個分佈式流媒體平臺。Kafka將記錄流存儲在稱爲主題的類別中。它能夠爲記錄的生產者和使用者提供線性水平可伸縮性,同時提供高可靠性和耐用性。分區,副本和代理是它提供大規模分佈式併發的一些基本概念。

6.4 緩存層

現代世界中沒有任何旨在實現高併發性的Web應用程序可以承受每次訪問數據庫的負擔。這就讓咱們選擇了一個緩存-----最好是能夠支持咱們高度併發的應用程序的內存中緩存:

  • Hazelcast 是一個分佈式,雲友好的,內存中的對象存儲和計算引擎,支持多種數據結構,如地圖,設置,列表,多重映射, RingBuffer和 HyperLogLog。它具備內置的複製功能,並提供高可用性和自動分區。
  • Redis 是一種內存數據結構存儲,咱們主要將其用做緩存。它提供了一個內存鍵值數據庫,具備可選的持久性。支持的數據結構包括字符串,哈希,列表和集合。Redis具備內置的複製功能,並提供高可用性和自動分區。若是咱們不須要持久性,Redis能夠爲咱們提供功能豐富,網絡化的內存中緩存,並具備出色的性能。

固然,在咱們追求構建高度併發的應用程序時,咱們幾乎沒有涉及任何可用的內容。重要的是要注意,除可用軟件外,咱們的要求還應指導咱們建立適當的設計。這些選項中的某些選項可能適用,而其餘選項可能不合適。

並且,別忘了還有更多可用的選項可能更適合咱們的要求。

7.結論

在本文中,咱們討論了併發編程的基礎。咱們瞭解了併發的一些基本方面及其可能致使的問題。此外,咱們演示了一些設計模式,這些模式能夠幫助咱們避免併發編程中的典型問題。

最後,咱們介紹了一些可用於構建高度並行的端到端應用程序的框架,庫和軟件。

相關文章
相關標籤/搜索