高併發應用的設計原則和模式

一、概述

在本教程中,我們將討論一些隨着時間的推移而建立的用於構建高度併發應用程序的設計原則和模式。

然而,值得注意的是,設計併發應用程序是一個廣泛而複雜的主題,因此沒有任何教程可以聲稱其處理是詳盡無遺的。我們將在這裏介紹的是一些經常使用的流行技巧!

  1. 併發基礎 =======

在我們繼續之前,讓我們花一些時間瞭解基礎知識。首先,我們必須澄清我們對併發程序的理解。如果多個計算同時發生,我們稱程序爲併發。

現在,請注意我們提到了同時發生的計算——也就是說,它們同時進行。但是,它們可能會或可能不會同時執行。理解其中的區別很重要,因爲同時執行的計算稱爲並行。

2.1. 如何創建併發模塊?

瞭解我們如何創建併發模塊很重要。有很多選擇,但我們將在這裏重點介紹兩個流行的選擇:

2.2. 併發模塊如何交互?

如果併發模塊不必通信是非常理想的,但通常情況並非如此。這產生了兩種併發編程模型:

2.3. 併發模塊如何執行?

摩爾定律在處理器時鐘速度方面遇到瓶頸已經有一段時間了。相反,由於我們必須成長,我們已經開始將多個處理器封裝到同一個芯片上,通常稱爲多核處理器。但是,仍然很少聽說有超過 32 個內核的處理器。

現在,我們知道單個內核一次只能執行一個線程或一組指令。但是,進程和線程的數量可能分別爲數百和數千。那麼,它到底是如何工作的呢?這是操作系統爲我們模擬併發的地方。操作系統通過時間片來實現這一點——這實際上意味着處理器在線程之間頻繁、不可預測和不確定地切換。

  1. 併發編程中的問題 ===========

當我們着手討論設計併發應用程序的原則和模式時,首先了解典型問題是什麼是明智的。

在很大程度上,我們的併發編程經驗涉及使用具有共享內存的本機線程。因此,我們將重點關注由此產生的一些常見問題:

  1. 高併發的設計模式 ===========

現在,我們瞭解了併發編程的基礎知識和其中的常見問題,是時候瞭解一些避免這些問題的常見模式了。我們必須重申,併發編程是一項需要大量經驗的艱鉅任務。因此,遵循一些既定模式可以使任務更容易。

4.1. 基於 Actor 的併發

我們將討論的關於併發編程的第一個設計稱爲 Actor 模型。這是一個併發計算的數學模型,基本上將所有事物都視爲一個 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 通過消息進行通信,每個 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 這樣的非阻塞數據結構可以幫助我們編寫線程安全的代碼,而不會沉迷於鎖的弊端!

  1. 編程語言支持 =========

我們已經看到有多種方法可以構建併發模塊。雖然編程語言確實有所不同,但主要是底層操作系統如何支持這個概念。然而,由於本機線程支持的基於線程的併發性在可伸縮性方面遇到了新障礙,我們總是需要新的選擇。

實施我們在上一節中討論的一些設計實踐確實被證明是有效的。但是,我們必須記住,它確實使編程本身變得複雜。我們真正需要的是能夠提供基於線程的併發能力而又不會帶來不良影響的東西。

我們可用的一種解決方案是綠色線程 (green threads)。綠色線程是由運行時庫調度的線程,而不是由底層操作系統本地調度的線程。雖然這並不能消除基於線程的併發中的所有問題,但在某些情況下它肯定可以爲我們提供更好的性能。

現在,使用綠色線程並非易事,除非我們選擇使用的編程語言支持它。並非每種編程語言都有這種內置支持。此外,我們粗略地稱爲綠色線程的東西可以由不同的編程語言以非常獨特的方式實現。讓我們看看其中一些可供我們使用的選項。

5.1. Go 中的協程

Go 編程語言中的 Goroutines 是輕量級線程。它們提供可以與其他函數或方法同時運行的函數或方法。Goroutines 非常便宜,因爲它們只佔用幾千字節的堆棧大小,以.

最重要的是,goroutines 與較少數量的本機線程複用。此外,goroutines 使用通道相互通信,從而避免訪問共享內存。我們得到了幾乎所有我們需要的東西,猜猜是什麼——什麼都不做!

5.2. Erlang 中的進程

在 Erlang 中,每個執行線程稱爲一個進程。但是,這與我們目前討論的過程不太一樣!Erlang 進程重量輕,內存佔用小,創建和處理速度快,調度開銷低。

在幕後,Erlang 進程只不過是運行時爲其處理調度的函數。此外,Erlang 進程不共享任何數據,它們通過消息傳遞相互通信。這就是爲什麼我們首先稱這些爲 “過程” 的原因!

5.3. Java 中的纖程(提案)

Java 併發的故事一直在不斷髮展。Java 確實支持綠色線程,至少對於 Solaris 操作系統,一開始是這樣。但是,由於超出本教程範圍的障礙,這已經停止。

從那時起,Java 中的併發就是關於本機線程以及如何巧妙地使用它們!但出於明顯的原因,我們可能很快就會在 Java 中擁有一個新的併發抽象,稱爲纖程。Project Loom 提議引入 continuations 和 fibers,這可能會改變我們用 Java 編寫併發應用程序的方式!

這只是對不同編程語言可用內容的初步瞭解。其他編程語言嘗試使用更有趣的方式來處理併發。

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

  1. 高併發應用 ========

真實世界的應用程序通常有多個組件通過網絡相互交互。我們通常通過 Internet 訪問它,它由代理服務、網關、Web 服務、數據庫、目錄服務和文件系統等多種服務組成。

在這種情況下我們如何保證高併發呢?讓我們探索其中的一些層以及我們用於構建高度併發應用程序的選項。

正如我們在上一節中看到的,構建高併發應用程序的關鍵是使用那裏討論的一些設計概念。我們需要爲工作選擇合適的軟件——那些已經包含其中一些實踐的軟件。

6.1. 網絡層

Web 通常是用戶請求到達的第一層,這裏不可避免地要提供高併發性。讓我們看看有哪些選項:

6.2. 應用層

在設計應用程序時,有幾種工具可以幫助我們構建高併發。讓我們檢查一些可供我們使用的庫和框架:

6.3. 數據層

最後,沒有數據的應用程序是不完整的,數據來自持久存儲。當我們討論與數據庫有關的高併發時,大部分的焦點都集中在 NoSQL 系列上。這主要是由於 NoSQL 數據庫可以提供線性可擴展性,但在關係變體中很難實現。讓我們看一下兩個流行的數據層工具:

6.4. 緩存層

好吧,現代世界中沒有任何以高併發爲目標的 Web 應用程序能夠承受每次都訪問數據庫。這讓我們選擇一個緩存 - 最好是可以支持我們的高併發應用程序的內存緩存:

當然,在我們追求構建高度併發的應用程序的過程中,我們僅僅觸及了可用資源的皮毛。重要的是要注意,除了可用的軟件之外,我們的需求應該指導我們創建合適的設計。其中一些選項可能合適,而另一些可能不合適。

而且,我們不要忘記還有更多可用選項可能更適合我們的要求。

七、結論

在本文中,我們討論了併發編程的基礎知識。我們瞭解併發的一些基本方面及其可能導致的問題。此外,我們還介紹了一些可以幫助我們避免併發編程中的典型問題的設計模式。

最後,我們瞭解了一些可用於構建高度併發的端到端應用程序的框架、庫和軟件。

原文:
https://www.baeldung.com/concurrency-principles-patterns

來源:

https://www.toutiao.com/article/7180705623687070219/?log_from=91338ae9da3d9_1672195519303

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/34GIpZUvHapCMpB8OGHRjw