閱讀199 返回首頁    go 阿裏雲 go 技術社區[雲棲]


並發處理中的問題以及解決這些問題的並發模型

單機並發是集群並發的基礎。本文主要將單機並發問題,和解決這些單機並發問題的解決模型。本文隻討論單機並發,集群並發將在我的後續其他文章中討論,所以本文將單機並發簡化稱為並發,省去單機二字。

1. 並發問題

什麼並發問題,舉個例子,一個服務器,有大量的鏈接上來,每個鏈接同時發請求。另外一種情況,隻有一個鏈接到服務器,但這個鏈接短時間內發送大量的請求。有些人隻是把第一種場景稱之為並發,這種場景多是直接麵向用戶的,比如web服務器,但是第二種場景也是並發,比如SOA架構中的服務。

這兩種的並發是有區別,而且有很多種方式來實現解決,這裏可以參看我的關於IO模型的討論。但是這裏我們采用一種統一的方式來處理,即將每個鏈接上的請求放入一個隊列,如何高效的將所有請求放入隊列可以參考IO模型的討論。這是一種非常常見的處理方式,大多數服務器和服務框架都采用這種方式。

從隊列中取出請求進行計算處理是並發的另外一部分,本文討論的就是這一部分。所以並發就是同時處理多個請求,如何提高同時處理請求的數量就是並發問題。

提高並發首先要知道我們要解決哪些問題,並發問題隱含以下3個問題:

  • 1.多路執行問題。
  • 2.多路間的通信問題。
  • 3.調用問題(包括耗時阻塞調用問題,並且調用存在多個,之間存在複雜的串並行關係)。

1.1 什麼是多路執行問題

要提高並發能力最基本的方式就是同時多路執行。多路執行是邏輯上的概念,往簡單裏說就是多進程執行或者多線程執行等。這是往簡單裏說,實際上多路執行是一個比較複雜的問題,後續會有解釋。

1.2 什麼是多路間的通信問題

有了多路執行,但是每一路執行都不是孤立,比如都需要一些共同的數據,或者一路的執行需要另一路執行提供數據。那麼這就需要多路間的通信。比如,如果是多進程方式的多路執行,就是進程間通信問題。

1.3 什麼是調用問題

並發問題並不是一個單純獨立的問題,實際的並發問題往往是一個很複雜的問題。比如網絡服務,提高一個網絡服務的並發能力,這個網絡服務接收到一個請求後還要請求其他網絡服務才能完成這個請求,請求其他網絡服務往往是一個耗時的阻塞調用。要提高並發能力,就需要解決耗時阻塞調用問題。並且在實際問題中,這些調用有可能存在多個,並且多個調用間可能還存在複雜的串並行關係。

下麵會討論如何解決這些問題,並且針對這些問題總結出問題解決模型。三個問題各自有各自的解決模型。

2. 多路執行問題的解決模型

2.1 兩種模型

解決多路執行問題的模型有2個:

  • 多線程/進程(即物理線程/進程)模型
  • 用戶態線程/輕量級線程(進程)模型

實現多路執行,自然想到的就是使用多進程或者多線程來實現,這也是最常見的一種解決模型。這種模型中的線程和進程是物理線程和物理進程,"物理"是指操作係統的提供的。一個物理線程/進程就是一路執行。各種語言都會實現這種模型。

我們也可以使用一個物理線程實現多路執行,即物理線程在不同的執行間進行切換。一般來講,這種同一個物理線程裏的不同執行會被稱為輕量級線程,即在一個物理線程中模擬出多個用戶態線程或者叫輕量級線程。想對於物理線程,這種輕量級線程,不需要操作係統做切換,即切換時不需要從操作係統的用戶態轉入的內核態,所以這種輕量級線程,也叫做用戶態線程。在erlang中,也有輕量級的概念,但是erlang是輕量的進程,但本質上和輕量級線程是一樣的。這三種叫法:用戶態線程,輕量級線程,輕量級進程,本質上來講是一樣的,所以本文後續隻用用戶態線程這種叫法。

2.2 用戶態線程模型的具體說明

2.2.1 用戶態線程的實現方式

用戶態線程是通過切分物理線程的方式實現的。
用戶態線程的本質就是切分物理線程。

不同的語言實現用戶態線程的方式不同:

  • C/C++中的用戶態線程

    C/C++中是通過協程技術實現的用戶態線程,詳細的描述可以參看我關於c++協程的文章。

  • erlang中的用戶態線程

    erlang中有輕量級進程的概念,是基於虛擬機實現的。

  • go中的用戶態線程

    go語言中的用戶態線程叫做goroutine,是基於協程技術實現的

  • scala中的用戶態線程

    scala中的用戶態線程叫做actor

  • Java中的用戶態線程

    Java可以采用第三方庫實現用戶態線程,詳細的描述可以參看我關於Java並發的文章。

2.2.1.1 用戶態線程與協程的關係

可以看到很多語言中的用戶態線程都采用協程技術,但是協程並不等價與用戶態線程,用戶態線程也是基於協程實現的,剛剛我們說了用戶態線程的本質是線程切分。

比如scala中的actor,因為scala是也基於java的底層庫,Java中是沒有內置支持協程的,所以actor的實現就不是基於協程的。scala中acotor的實現類似於這樣的實現:

通常都是建立任務隊列,1個線程或多個線程,或者線程池,從隊列中取出並且執行這些任務,每個任務相當與一個actor。

在這樣的實現中任務是順序執行的,也就是說任務不能在中途切換到另一個任務。協程技術可以實現在任務的中途切換到另一個任務。也就是說協程的本質可以說是一種用戶態線程的切換機製。詳細的關於協程的描述可以參看我關於協程的文章。

2.2.2 用戶態線程調度策略

用戶態線程有三種調度策略:

  • 1.順序的調度策略(在隻實現了任務隊列+線程池的實現方式中,任務是順序執行的,比如,scala的actor)
  • 2.協作式線程調度,基於協程的用戶線程切換
  • 3.輪詢的調度策略(erlang的輪轉調度策略,go 1.2後具有簡單搶占機製的調度策略)

2.2.3 用戶態線程抽象模型

無論用什麼方法實現用戶態線程,最終都需要通過物理線程實現。所以用戶態線程一定和物理線程有一定的對應關係,也即用戶態線程抽象模型,抽象模型包括三種:
1:1
N:1
n:m

1:1
即一個物理線程抽象成一個用戶態線程
N:1
一個物理線程模擬N個用戶態線程
N:M
m個物理線程模擬n個用戶態線程,比如go中的pmg的概念,更複雜的3元抽象

3.阻塞調用問題的解決模型

解決阻塞調用問題,通常有三種方式,也即有三種解決模型:

  • 1.多線程模型(保持阻塞用多線程規避)
  • 2.回調模型
  • 3.協程切換模型

第一種模型的解決方式是,遇到阻塞調用時,保持阻塞調用,也即不做任何處理,而是采用啟動多個線程的方式提高並發能力。
第二種回調模型,采用異步技術,將阻塞調用變成異步調用,當調用完成時通過回調的方式通知調用者。這樣一個線程就可以同時處理多個並發請求。這種模型是一種最高效的模型。避免的過多的物理線程頻繁切換帶來的不必要的開銷。
第三種協程切換模型,也采用異步技術,將阻塞調用變成異步調用,所以從效率上來講,這種模型的效率並部高於第二種模型,為什麼會出現這種模型,是因為第二種回調模型編程複雜。協程切換模型簡化了這種複雜性。

通常這三種模型被總結成三種並發模型,網上對並發模型的分類: 多線程模型,異步回調模型,輕量級線程/協程模型。

我們繼續文章最開始的例子,請求被放入一個隊列中,我們分以下接個討論,看看如何並發處理這些隊列。在處理實際請求時,可能需要訪問數據庫,調用其他服務等等,這些操作都可以簡化成,向網絡發送一個請求,再從網絡接收一個回複。

3.1 多線程模型示例

偽代碼如下:

main_processing()
{
    loop
    {
        request = queue.get();
        create_thread (handle_request, request);
    }
}

handle_request(request)
{
    sendto(server1);
    receivefrom(server1);

    sendto(server2);
    receivefrom(server2);

    sendto(server3);
    receivefrom(server3);
}

在請求處理函數中,receivefrom()是阻塞調用。

3.2 回調模型示例

這種模型中引入了異步技術。

非常簡略的示意偽代碼:

request_process_loop()
{
    loop
    {
        if (!queue.empty())
        {
            request = queue.get();
            handle_request(request);
        }
    }
}

handle_request(request)
{
    sendto (server1, request, handle_request_callback1);
}

handle_request_callback1(response)
{
    sendto (server2, request, handle_request_callback2);
}

handle_request_callback2(response)
{
    sendto (server3, request, handle_request_callback3);
}

handle_request_callback3(response)
{
    print (response.message);
}

在這種模型中send是異步的,並且傳入了一個回調函數,不需要顯示調用recieve,係統在收到response時,會調用回調函數。這裏省略了采用IO複用機製,調用callback函數的細節。具體的技術細節參看IO模型。
這種模型種不會引入過多線程,一般一個線程就可以處理並發請求,處理效率是最高的。但也可以看到同樣的業務邏輯不得不分散到3個回調函數中,編程複雜。

3.3 協程切換模型

這種模型也引入了異步技術。同時采用協程技術避免了callback。

非常簡略的示意偽代碼:

main_processing()
{
    loop
    {
        request = queue.get();
        create_coroutine (handle_request, request);
    }
}

handle_request(request)
{
    sendto(server1);
    reponse = switchout();

    sendto(server2);
    reponse = switchout();

    sendto(server3);
    reponse = switchout();
}

recieve_processing()
{
    loop
    {
        recievefrom( any );
        continue_coroutine();
    }
}

這裏省略的很多實現的細節。但是通過協程技術,即保證了處理請求的並發性,同時也降低了編程的複雜度,基本和多線程模型的難度是一樣的。實際上,在具體的語言和框架中,通過封裝完全可以達到使用上與多線程模型完全一致,采用同一種"並發編程模型",雖然底層采用了完全不同的"並發模型"。在本文的第6節,會描述並發編程模型。

4. 多路間通信問題的解決模型

多路間通信問題的解決模型有2種:

  • 1.線程同步機製模型

    這種模型是基於共享內存和線程間的同步機製(即各種鎖)

  • 2.message傳遞模型

    這種模型采用message傳遞的方式來進行多路間的通信。這種模型有兩種具體的實現方式(也即有兩種編程模型,在第6節會詳細描述):actor模型,SCP模型。

5. 並發技術

上麵提到了並發處理模型中使用的三種主要技術:
多核/多線程(進程)技術
異步技術
協程技術

這3種技術在鬢發處理模型中分別起到了不同的作用:
多核/多線程(進程)技術:提高同時處理的數量
異步技術:降低不必要的消耗
協程技術:降低編程難度

6. 並發編程模型

以上討論的並發處理的模型,這些模型在不同的語言和框架中被設計成不同的形式,有不同的使用接口和方式。這些不同的使用方式就是並發編程模型。

6.1 多線程+線程同步機製模型

多線程+線程同步機製的編程模型是邏輯上最自然的的編程模型,也是最普通的編程模型,最容易理解。

在這種模型中,要並發處理任務就創建線程,線程間的通信采用共享內存和線程同步機製。如在第三節中的討論,雖然使用方式和接口都一樣,但是底層的實現可以采用完全不同的並發模型,所以這種編程模型中,的線程可以是物理線程,也可以是用戶態線程。

在這種模型可以從這種多線程模型演變成線程池模型,但本質上是一樣的。

在第7節,Java1.0和c++ threadstate庫都屬於這種編程模型。

6.2 線程池+任務+Future/Promise的編程模型

這種編程模型主要聚焦在線程同步機製。線程同步機製即各種鎖,必須正確使用才能避免死鎖等各種問題。這種編程模式就是提出了一種線程同步方式,也即總結一種比較常見的使用場景,提出了一個模式(pattern),簡化線程同步。

一般這種的模式的接口定義如下:

Promise<T>
{
    Future Get_future();
    Set_value(T);
}

Future<T>
{
    T Get_value();
    Wait();
}

這種模式還有進一步的演化,就是Callback chain by then,添加新接口如下:

Future<T>
{
    Then(AsyncFunc) ;
}

這裏我們就不舉例子,在第7節,我們會看到Java和scala是如何實現這種編程模型的。

6.3 基於事件+狀態機的編程模型

我們在3.2節中看到,基於回調的並發模型,在編程模型層麵,我們可以把callback抽象成事件,在複雜的業務邏輯中,很難控製callback間的跳轉,這時我們可以引入有限狀態機來進行管理。

6.4 Actor模型

在Actor模型中,actor是一個獨立的單元,是一個用戶態的線程,actor與actor之間采用message進行多路間的通信。message傳遞的機製是每個actor內部都有一個mailbox,actor可以向其他actor的mailbox投遞消息,每個actor隻能從自己的mailbox中取消息進行處理。

Actor模型的典型的實現是scala和erlang。其他語言也有對actor模型的實現,在第7節會詳細描述各語言的實現。

6.5 CSP模型

CSP(Communicating Sequential Processes)也是一種基於message傳遞的並發編程模型。與actor模型的不同之處在於,在CSP中有2中角色,worker和channel。worker是用戶態線程,channel用戶message傳遞。

CSP模型是golang采用的編程模型。

7. 實際的例子

以上都是一些理論上的討論,下一些比較典型的例子說明各種語言和框架對並發的支持。

7.1 Java

物理線程,線程同步機製

引入了ExecutorService, Callable, TaskFuture。

但是沒有解決阻塞調用的問題

7.2 Node.js

7.3 Scala/Akka

基於java concurrent實現的logic線程,即actor,基於消息傳遞的mailbox

7.4 c++ StateThread 協程庫

基於異步和協程技術,實現了多線程+線程同步機製模型。

這個種提供了線程管理、線程同步、網絡訪問等方法接口。

比如:

st_thread_create創建一個新的用戶線程

st_read從網絡中讀取,這個操作會阻塞用戶線程,但通過協程技術,物理線程切換到另外一個可運行的用戶態線程繼續執行

7.5 Go

協程,對阻塞係統調用的hack處理的green化方式,基於channel的消息傳遞模型

最後更新:2017-04-26 18:00:56

  上一篇:go JS基礎知識概述
  下一篇:go 先別急著唱高互聯網醫療,發展還得再過“三道坎”