使用者態併發 基本框架

2021-06-25 11:42:59 字數 3373 閱讀 9241

為了結合兩者的優點,需要在語言實現上下功夫,具體地說,就是在寫源語言的時候,可以像很多流行的語言一樣建立執行緒,使用方式也一樣,但源語言的執行緒並不對應宿主環境的真執行緒,宿主環境可以自始至終單執行緒執行,在執行時對源語言的邏輯執行緒進行排程,具體實現跟協程和執行緒都有相似的地方,簡單說就是在虛擬機器實現乙個簡化版的小型作業系統,只不過這個os主要作用是排程,其他細節如硬體驅動和記憶體管理則直接使用宿主環境了。因此本文討論的是在虛擬機器環境下的使用者態執行緒併發,跟宿主環境的真實執行緒沒有關係,可以叫偽執行緒,下述**和描述中的thread和執行緒都是指偽執行緒。另外,簡單起見我們忽略異常機制等細枝末節的實現

這個虛擬機器仿照作業系統相關部分來做,跟前面講過的乙個例子一樣,虛擬機器中所有執行緒都是對等的,即main執行緒也需要建立,虛擬機器入口是乙個排程主迴圈:

... //必要的初始化工作 

thread main_thread = new thread(code.main_func); //建立主線程,只是個物件而已

env.thread_table.add(main_thread); //主線程加入執行緒列表

env.running_queue.add(main_thread); //主線程可以立即開始執行

while (env.thread_table.size() > 0) //開始排程

thread_table.erase(t); //執行緒結束,去掉它

} case thread.stat_yield:

... //可能還有其他情況,不過一般來說執行緒返回只有上面兩種了,跟協程乙個道理

} } //執行結束,虛擬機器退出

在這裡,每乙個thread是乙個自動機,在主迴圈中的事情非常簡單,找到乙個當前可以執行的執行緒t,呼叫t.run(),在run中會從t的當前狀態(也就是上次run返回時的狀態)開始執行,直到執行緒結束或需要切換,run返回後,主迴圈判斷執行緒返回狀態,若執行緒是主動放棄(yield),則切換下乙個t執行,若是執行緒結束,則處理善後工作

若不考慮阻塞(比如所有執行緒都是計算密集型**,且互不相關),則只需要乙個running佇列即可,每個執行緒執行一段時間,執行標準排程返回stat_yield,切換到另乙個,反覆如此直到所有執行緒都執行完畢,這是非常簡單的,或者我們可以乾脆不考慮標準排程,則各個執行緒是序列執行的,執行main_thread的run的時候建立的執行緒都只是加入thread_table和running佇列,在main執行結束後挨個進行。顯然,我們使用執行緒不是為了序列計算

回想下os對執行緒的排程方式,每個執行緒有自己的棧空間,棧的內容可以看做是它的主要狀態之一,在切換上下文的時候,保持當前棧不變,修改暫存器環境,直接jmp到另乙個執行緒的指令處,和上篇說的協程用goto自由跳轉實現一樣

在上面的虛擬機器模型中,情況也是類似,使用t.run()來代替直接jmp,而每個執行緒的棧則儲存在thread物件內部,這意味著無論我們是否通過遞迴呼叫execute來實現函式呼叫,execute內部不能再儲存任何和狀態相關的資料了(區域性變數,指令索引,運算棧等),統統都需要放在thread物件裡面,實際上execute裡只能有一些虛擬機器內部使用的臨時變數。thread物件中儲存的執行緒棧可以用鍊錶,用多少申請多少,動態增減,這樣就解決了前述os級執行緒位址空間占用的問題,當然會造成一些效能損耗,但是可以通過使用者態排程彌補回來

實現執行緒狀態和**分離,只需要將位元組碼解釋的execute放在thread類中,或將當前執行緒t作為引數傳給execute。函式呼叫的棧幀用frame類:

class frame 

; thread中則使用lifo的資料結構存frame層次關係,比如用vector(鍊錶也行):

vectorframe_stk;

從使用上來說,run執行的時候分兩種情況,第一次執行和繼續上次斷點執行,但是由於初始狀態也是一種斷點,因此就不做區分了,在run方法中只是簡單呼叫execute:

void run() 

else

}

考慮一下,乙個執行緒yield的時候,斷點可能在乙個比較深的函式呼叫,雖然frame_stk把資料和執行點儲存下來了,但由於使用的宿主語言不支援直接jmp,而是先設定this.stat,然後逐層返回,最後直到run返回,那麼反過來,從斷點開始繼續執行時,要恢復宿主語言原先的呼叫棧,因此這個地方需要對位元組碼code_call_func做特殊處理:

首先約定execute的傳入引數是current_frame_idx,然後:

frame current_frame = this.frame_stk[current_frame_idx];
接著是code_call_func的實現:

case code_call_func: 

else

//無論是恢復還是正常呼叫,在這裡的狀態都一致了

this.execute(current_frame_idx + 1); //進入下乙個呼叫棧

//呼叫結束或中斷,檢查

if (this.stat == stat_running)

//走到這裡說明yield了

-- current_frame.idx; //根據前面幾篇**的約定,idx這時候指向下一條指令,恢復到當前指令

return; //直接返回

}

最後還需要稍稍改一下code_return:

case code_return: 

這段偽**如果正確實現,完成功能應該是沒問題的,不過我寫完了發現有個槽點,判斷是否處於斷點恢復狀態的**其實可以在execute執行一開頭就做了,這樣code_call_func的**能更清晰些,而且不用恢復idx,不過這點懶得改了;但另一點,由於這裡的實現還是用宿主語言的execute遞迴來實現源語言的函式呼叫,因此每次yield和恢復都會有消耗(如果呼叫棧比較深),更好的做法我覺得是改進編譯器,將code_call_func和code_return都通過壓棧和code_jmp來實現(就像彙編那樣),這樣一來宿主語言的run只需要呼叫一層execute

p.s.還有乙個可能有改進空間的細節,這裡我們把運算棧放在棧幀中,而事實上如果虛擬機器沒有bug,則所有呼叫可共用乙個運算棧,手工模擬下就知道這是沒有問題的(還覺得不保險可以加個特殊的分界元素);另外,區域性變數和運算棧也分開了,這個也是可以合併的

這裡因為不考慮異常機制等細節,execute返回只有兩種情況,finish和yield,其中finish時的處理上面寫得很清楚了,返回值存在當前棧幀後直接return,由呼叫者負責銷毀被呼叫者的棧幀,yield的情況則更簡單:

this.stat = stat_yield; 

return;

只要當前棧幀的狀態沒有錯,下次就可以恢復了

在這個框架下,標準排程很容易實現,就不囉嗦了

併發與競態

linux是乙個多工的作業系統,在多個程序同時執行時,就有可能為了競爭同乙個資源發生堵塞。以下是解決的幾種方法 1 訊號量 declare mutex sem if down interruptible sem critical section up sem 2 完成量 declare comple...

LDD 併發和競態

1.正在執行的多個使用者空間程序可能以一種令人驚訝的組合方式訪問我們的 2.smp系統甚至可在不同的處理器上同時執行我們的 3.核心是可搶占的,驅動程式 可能在任何時候丟失對處理器的獨佔 4.裝置中斷時非同步事件,可能導致 的併發執行 5.核心還提供了許多可延遲 執行的機制,比如workqueue ...

使用者認證 登入態

authentication 使用者認證,指的是驗證使用者的身份,例如你希望以小a的身份登入,那麼應用程式需要通過使用者名稱和密碼確認你真的是小a。由於http協議是無狀態的,每一次請求都無狀態。當乙個使用者通過使用者名稱和密碼登入了之後,他的下乙個請求不會攜帶任何狀態,應用程式無法知道他的身份,那...