天天看點

并發進行中的問題以及解決這些問題的并發模型1. 并發問題2. 多路執行問題的解決模型3.阻塞調用問題的解決模型4. 多路間通信問題的解決模型5. 并發技術6. 并發程式設計模型7. 實際的例子

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

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

這兩種的并發是有差別,而且有很多種方式來實作解決,這裡可以參看我的關于io模型的讨論。但是這裡我們采用一種統一的方式來處理,即将每個連結上的請求放入一個隊列,如何高效的将所有請求放入隊列可以參考io模型的讨論。這是一種非常常見的處理方式,大多數伺服器和服務架構都采用這種方式。

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

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

1.多路執行問題。

2.多路間的通信問題。

3.調用問題(包括耗時阻塞調用問題,并且調用存在多個,之間存在複雜的串并行關系)。

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

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

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

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

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

多線程/程序(即實體線程/程序)模型

使用者态線程/輕量級線程(程序)模型

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

我們也可以使用一個實體線程實作多路執行,即實體線程在不同的執行間進行切換。一般來講,這種同一個實體線程裡的不同執行會被稱為輕量級線程,即在一個實體線程中模拟出多個使用者态線程或者叫輕量級線程。想對于實體線程,這種輕量級線程,不需要作業系統做切換,即切換時不需要從作業系統的使用者态轉入的核心态,是以這種輕量級線程,也叫做使用者态線程。在erlang中,也有輕量級的概念,但是erlang是輕量的程序,但本質上和輕量級線程是一樣的。這三種叫法:使用者态線程,輕量級線程,輕量級程序,本質上來講是一樣的,是以本文後續隻用使用者态線程這種叫法。

使用者态線程是通過切分實體線程的方式實作的。

使用者态線程的本質就是切分實體線程。

不同的語言實作使用者态線程的方式不同:

c/c++中的使用者态線程

erlang中的使用者态線程

go中的使用者态線程

scala中的使用者态線程

java中的使用者态線程

可以看到很多語言中的使用者态線程都采用協程技術,但是協程并不等價與使用者态線程,使用者态線程也是基于協程實作的,剛剛我們說了使用者态線程的本質是線程切分。

比如scala中的actor,因為scala是也基于java的底層庫,java中是沒有内置支援協程的,是以actor的實作就不是基于協程的。scala中acotor的實作類似于這樣的實作:

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

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

使用者态線程有三種排程政策:

1.順序的排程政策(在隻實作了任務隊列+線程池的實作方式中,任務是順序執行的,比如,scala的actor)

2.協作式線程排程,基于協程的使用者線程切換

3.輪詢的排程政策(erlang的輪轉排程政策,go 1.2後具有簡單搶占機制的排程政策)

無論用什麼方法實作使用者态線程,最終都需要通過實體線程實作。是以使用者态線程一定和實體線程有一定的對應關系,也即使用者态線程抽象模型,抽象模型包括三種:

1:1

n:1

n:m

即一個實體線程抽象成一個使用者态線程

一個實體線程模拟n個使用者态線程

m個實體線程模拟n個使用者态線程,比如go中的pmg的概念,更複雜的3元抽象

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

1.多線程模型(保持阻塞用多線程規避)

2.回調模型

3.協程切換模型

第一種模型的解決方式是,遇到阻塞調用時,保持阻塞調用,也即不做任何處理,而是采用啟動多個線程的方式提高并發能力。

第二種回調模型,采用異步技術,将阻塞調用變成異步調用,當調用完成時通過回調的方式通知調用者。這樣一個線程就可以同時處理多個并發請求。這種模型是一種最高效的模型。避免的過多的實體線程頻繁切換帶來的不必要的開銷。

第三種協程切換模型,也采用異步技術,将阻塞調用變成異步調用,是以從效率上來講,這種模型的效率并部高于第二種模型,為什麼會出現這種模型,是因為第二種回調模型程式設計複雜。協程切換模型簡化了這種複雜性。

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

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

僞代碼如下:

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

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

非常簡略的示意僞代碼:

在這種模型中send是異步的,并且傳入了一個回調函數,不需要顯示調用recieve,系統在收到response時,會調用回調函數。這裡省略了采用io複用機制,調用callback函數的細節。具體的技術細節參看io模型。

這種模型種不會引入過多線程,一般一個線程就可以處理并發請求,處理效率是最高的。但也可以看到同樣的業務邏輯不得不分散到3個回調函數中,程式設計複雜。

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

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

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

1.線程同步機制模型

2.message傳遞模型

上面提到了并發處理模型中使用的三種主要技術:

多核/多線程(程序)技術

異步技術

協程技術

這3種技術在鬓發處理模型中分别起到了不同的作用:

多核/多線程(程序)技術:提高同時處理的數量

異步技術:降低不必要的消耗

協程技術:降低程式設計難度

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

多線程+線程同步機制的程式設計模型是邏輯上最自然的的程式設計模型,也是最普通的程式設計模型,最容易了解。

在這種模型中,要并發處理任務就建立線程,線程間的通信采用共享記憶體和線程同步機制。如在第三節中的讨論,雖然使用方式和接口都一樣,但是底層的實作可以采用完全不同的并發模型,是以這種程式設計模型中,的線程可以是實體線程,也可以是使用者态線程。

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

在第7節,java1.0和c++ threadstate庫都屬于這種程式設計模型。

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

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

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

這裡我們就不舉例子,在第7節,我們會看到java和scala是如何實作這種程式設計模型的。

我們在3.2節中看到,基于回調的并發模型,在程式設計模型層面,我們可以把callback抽象成事件,在複雜的業務邏輯中,很難控制callback間的跳轉,這時我們可以引入有限狀态機來進行管理。

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

actor模型的典型的實作是scala和erlang。其他語言也有對actor模型的實作,在第7節會較長的描述各語言的實作。

csp(communicating sequential processes)也是一種基于message傳遞的并發程式設計模型。與actor模型的不同之處在于,在csp中有2中角色,worker和channel。worker是使用者态線程,channel使用者message傳遞。

csp模型是golang采用的程式設計模型。

以上都是一些理論上的讨論,下一些比較典型的例子說明各種語言和架構對并發的支援。

實體線程,線程同步機制

引入了executorservice, callable, taskfuture。

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

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

基于異步和協程技術,實作了多線程+線程同步機制模型。

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

比如:

st_thread_create建立一個新的使用者線程

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

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