Go協程阻塞太長時間會發生什麼?

2021-10-24 00:24:46 字數 1851 閱讀 2482

執行緒排程由os全權負責,執行緒遇到一些系統呼叫(比如i/o)時,可能阻塞,也可能立即返回。

那麼go協程如果阻塞太長時間,會發生什麼?

本文簡單講講go的gmp排程模型以及乙個特殊場景。

m (work thread),工作執行緒

p (processor),代表乙個處理器(不是物理處理器),又稱上下文

gmp模型通過任務佇列之間的切換與g的上下文恢復實現協程排程。

小結:gmp模型以任務隊列為主體,佇列內的物件是g,佇列的維護者(上下文切換)是p,執行者是m,佇列順序執行。具體的任務切換策略暫略。

協程的切換時間片是10ms,也就是說 goroutine 最多執行10ms就會被 m 切換到下乙個 g。這個過程,又被稱為中斷,掛起

假設m的任務佇列裡有g1、g2,g1遇到非同步系統呼叫時,m會先執行g2,g1非同步返回時,插入回m的任務佇列尾部。

假設m的任務佇列裡有g1、g2,g1遇到同步系統呼叫時,g1會被排程到另外乙個完全空閒的m上執行(如果不存在,則先建立乙個新的m),g1同步返回時,插入回m的任務佇列尾部。

當乙個m的任務佇列空閒時,可以從其他m或全域性任務佇列竊取任務。竊取優先順序是:

前面說的都是gmp排程,實際上是基於執行緒的使用者層排程。想象以下場景

多機器 * 多程序,都執行乙個goroutine,這個協程中間有乙個阻塞式操作(select … for update)。

如果a程序拿到mysql的鎖,a沒有rollback/commit 的情況下,b想拿鎖拿不到,在10ms後b的這個協程會被判定為阻塞(由於全域性佇列與所有m的任務佇列裡,僅有乙個協程任務,這時協程排程就退化為執行緒排程)。

判定阻塞的原理:

如何優化以上場景?

mysql 8.0支援select … for update nowait

當a事務沒有rollback/commit 的情況下,b事務試圖取鎖會直接返回錯誤。偽**如下:

//業務**前置的試圖取鎖

if !util.getlockfromlooperlocks("lock_data_looper", "update_task_status_access", tx)

sql_str := "select * from running_loop_locks where loop_name=? and lock_name=? for update nowait"

results, err := tx.query(sql_str, loop_name, lock_name)

defer results.close()

if err != nil

for results.next()

return true }}

//取鎖失敗則回滾

func (util *util) looperlockscommitorrollback(tx *sql.tx, ret int) (int, error) else if 0 == ret else

return 0, nil

}

以上**利用了mysql8.0 對 select …from … for update進行的擴充套件——開始支援for update nowait 和for update skip locked

nowait:表示無法獲取到鎖時直接返回錯誤,不用等待 innodb_lock_wait_timeout 的時間,再返回報錯,這樣就減少了執行緒的阻塞時間。

go 協程等待

sync包提供了基本的同步基元,如互斥鎖 但是這裡不是討論執行緒通訊的問題 而執行緒通訊應使用channel 以前使用time.sleep 來保證執行緒執行完成,顯然執行緒執行所需要的時間不確定 sync裡面有乙個waitgroup,它是乙個結構體,可以用於等待執行緒執行 這樣不用去估算執行緒需要執...

Go 協程 通道

目錄 go 協程 go 通道 go 協程go 協程可以看作是輕量級執行緒。與執行緒相比,建立乙個go協程的成本很小。因此在go應用中,常常會看到有數以千計的go協程併發地執行 go 協程相比於執行緒的優勢 啟動乙個go協程 在呼叫函式和方法時,在前面加上關鍵字go,可以讓乙個新的go協程併發執行 p...

協程中呼叫阻塞函式

from concurrent.futures import threadpoolexecutor from tornado import gen threadpool threadpoolexecutor 2 def mysleep count import time for i in range...