golang減少鎖的使用,golang 自旋鎖

本文目錄一覽:

哪些方法可以避免或減少鎖的使用

如果是為了防止進程間共享數據的衝突,避免或減少鎖的使用你可以試試這樣:

1,處理之前關中斷

2,使用測試並置位指令

3, 禁止任務切換

如果是為了防止家裡失竊,避免或減少鎖的使用你可以試試這樣:

多雇幾個保安

Go:互斥體和飢餓

在Golang中進行開發時,互斥鎖在不斷嘗試獲取永遠無法獲取的鎖時會遇到 飢餓 問題。在本文中,我們將探討影響Go 1.8的飢餓問題,該問題已在Go 1.9中解決。

為了說明互斥鎖的飢餓狀況,我將以 拉斯·考克斯 ( Russ Cox)提出的 關於他們討論互斥鎖改進的 問題 為例:

starvation.go

此示例基於兩個goroutine:

兩者都具有100微秒的周期,但是由於goroutine 1一直在請求鎖定,因此可以預期它將更頻繁地獲得鎖定。

這是一個用Go 1.8進行的示例,該示例具有10次迭代的循環的鎖分配:

該互斥鎖已被第二個goroutine捕獲了十次,而第一個則超過了700萬次。讓我們分析一下這裡發生了什麼。

首先,goroutine 1將獲得鎖定並睡眠100微秒。當goroutine 2嘗試獲取鎖時,它將被添加到鎖的隊列(FIFO順序)中,並且goroutine將進入等待狀態:

Figure 1 — lock acquisition

然後,當goroutine 1完成工作時,它將釋放鎖定。此版本將通知隊列喚醒goroutine 2。Goroutine 2將被標記為可運行,並正在等待Go Scheduler在線程上運行:

Figure 2— goroutine 2 is awoke

但是,在goroutine 2等待運行時,goroutine 1將再次請求鎖定:

Figure 3— goroutine 2 is waiting to run

當goroutine 2嘗試獲取鎖時,它將看到它已經具有保持狀態並進入等待模式,如圖2所示:

Figure 4— goroutine 2 tries again to get the lock

goroutine 2對鎖的獲取將取決於它在線程上運行所花費的時間。

現在已經確定了問題,讓我們回顧可能的解決方案。

處理互斥量的方法有很多,例如:

barging mode

Go 1.8就是這樣設計的,它反映了我們之前看到的內容。

handoff mode

我們可以 在Linux內核 的 互斥體中 找到此邏輯:

在我們的情況下,互斥鎖切換會完美平衡兩個goroutine之間的鎖分配,但是會降低性能,因為這將迫使第一個goroutine即使未持有也要等待鎖。

spinning mode

Go 1.8也使用此策略。當試圖獲取已經持有的鎖時,如果本地隊列為空且處理器數量大於一,則goroutine將旋轉幾次-如果僅使用一個處理器旋轉就會阻塞程序。旋轉後,goroutine將停放。如果程序大量使用鎖,它可以作為快速路徑。

有關如何設計鎖的更多信息( 插入 ,越區切換,自旋鎖),通常, Filip Pizlo撰寫 了必讀的文章「 WebKit中的鎖定 」。

在Go 1.9之前,Go結合了插入和旋轉模式。在1.9版中,Go通過添加新的飢餓模式解決了先前解釋的問題,該模式將導致在解鎖模式期間進行切換。

所有等待鎖定時間超過一毫秒的goroutine,也稱為 有界等待 ,將被標記為飢餓。當標記為飢餓時,解鎖方法現在將把鎖直接移交給第一位服務員。這是工作流程:

starvation mode

由於進入的goroutine將不會獲取任何為下一個服務員保留的鎖,因此在飢餓模式下也將禁用旋轉。

讓我們使用Go 1.9和新的starvation模式運行前面的示例:

現在的結果更加公平。現在,我們想知道新的控制層是否會對互斥體不處於飢餓狀態的其他情況產生影響。正如我們在該程序包的基準測試(Go 1.8與Go 1.9)中所看到的,在其他情況下,性能並沒有下降(不同處理器數量下, 性能會略有變化 ):

翻譯自:

(十一)golang 內存分析

編寫過C語言程序的肯定知道通過malloc()方法動態申請內存,其中內存分配器使用的是glibc提供的ptmalloc2。 除了glibc,業界比較出名的內存分配器有Google的tcmalloc和Facebook的jemalloc。二者在避免內存碎片和性能上均比glic有比較大的優勢,在多線程環境中效果更明顯。

Golang中也實現了內存分配器,原理與tcmalloc類似,簡單的說就是維護一塊大的全局內存,每個線程(Golang中為P)維護一塊小的私有內存,私有內存不足再從全局申請。另外,內存分配與GC(垃圾回收)關係密切,所以了解GC前有必要了解內存分配的原理。

為了方便自主管理內存,做法便是先向系統申請一塊內存,然後將內存切割成小塊,通過一定的內存分配演算法管理內存。 以64位系統為例,Golang程序啟動時會向系統申請的內存如下圖所示:

預申請的內存劃分為spans、bitmap、arena三部分。其中arena即為所謂的堆區,應用中需要的內存從這裡分配。其中spans和bitmap是為了管理arena區而存在的。

arena的大小為512G,為了方便管理把arena區域劃分成一個個的page,每個page為8KB,一共有512GB/8KB個頁;

spans區域存放span的指針,每個指針對應一個page,所以span區域的大小為(512GB/8KB)乘以指針大小8byte = 512M

bitmap區域大小也是通過arena計算出來,不過主要用於GC。

span是用於管理arena頁的關鍵數據結構,每個span中包含1個或多個連續頁,為了滿足小對象分配,span中的一頁會劃分更小的粒度,而對於大對象比如超過頁大小,則通過多頁實現。

根據對象大小,劃分了一系列class,每個class都代表一個固定大小的對象,以及每個span的大小。如下表所示:

上表中每列含義如下:

class: class ID,每個span結構中都有一個class ID, 表示該span可處理的對象類型

bytes/obj:該class代表對象的位元組數

bytes/span:每個span佔用堆的位元組數,也即頁數乘以頁大小

objects: 每個span可分配的對象個數,也即(bytes/spans)/(bytes/obj)waste

bytes: 每個span產生的內存碎片,也即(bytes/spans)%(bytes/obj)上表可見最大的對象是32K大小,超過32K大小的由特殊的class表示,該class ID為0,每個class只包含一個對象。

span是內存管理的基本單位,每個span用於管理特定的class對象, 跟據對象大小,span將一個或多個頁拆分成多個塊進行管理。src/runtime/mheap.go:mspan定義了其數據結構:

以class 10為例,span和管理的內存如下圖所示:

spanclass為10,參照class表可得出npages=1,nelems=56,elemsize為144。其中startAddr是在span初始化時就指定了某個頁的地址。allocBits指向一個點陣圖,每位代表一個塊是否被分配,本例中有兩個塊已經被分配,其allocCount也為2。next和prev用於將多個span鏈接起來,這有利於管理多個span,接下來會進行說明。

有了管理內存的基本單位span,還要有個數據結構來管理span,這個數據結構叫mcentral,各線程需要內存時從mcentral管理的span中申請內存,為了避免多線程申請內存時不斷的加鎖,Golang為每個線程分配了span的緩存,這個緩存即是cache。src/runtime/mcache.go:mcache定義了cache的數據結構

alloc為mspan的指針數組,數組大小為class總數的2倍。數組中每個元素代表了一種class類型的span列表,每種class類型都有兩組span列表,第一組列表中所表示的對象中包含了指針,第二組列表中所表示的對象不含有指針,這麼做是為了提高GC掃描性能,對於不包含指針的span列表,沒必要去掃描。根據對象是否包含指針,將對象分為noscan和scan兩類,其中noscan代表沒有指針,而scan則代表有指針,需要GC進行掃描。mcache和span的對應關係如下圖所示:

mchache在初始化時是沒有任何span的,在使用過程中會動態的從central中獲取並緩存下來,跟據使用情況,每種class的span個數也不相同。上圖所示,class 0的span數比class1的要多,說明本線程中分配的小對象要多一些。

cache作為線程的私有資源為單個線程服務,而central則是全局資源,為多個線程服務,當某個線程內存不足時會向central申請,當某個線程釋放內存時又會回收進central。src/runtime/mcentral.go:mcentral定義了central數據結構:

lock: 線程間互斥鎖,防止多線程讀寫衝突

spanclass : 每個mcentral管理著一組有相同class的span列表

nonempty: 指還有內存可用的span列表

empty: 指沒有內存可用的span列表

nmalloc: 指累計分配的對象個數線程從central獲取span步驟如下:

將span歸還步驟如下:

從mcentral數據結構可見,每個mcentral對象只管理特定的class規格的span。事實上每種class都會對應一個mcentral,這個mcentral的集合存放於mheap數據結構中。src/runtime/mheap.go:mheap定義了heap的數據結構:

lock: 互斥鎖

spans: 指向spans區域,用於映射span和page的關係

bitmap:bitmap的起始地址

arena_start: arena區域首地址

arena_used: 當前arena已使用區域的最大地址

central: 每種class對應的兩個mcentral

從數據結構可見,mheap管理著全部的內存,事實上Golang就是通過一個mheap類型的全局變數進行內存管理的。mheap內存管理示意圖如下:

系統預分配的內存分為spans、bitmap、arean三個區域,通過mheap管理起來。接下來看內存分配過程。

針對待分配對象的大小不同有不同的分配邏輯:

(0, 16B) 且不包含指針的對象: Tiny分配

(0, 16B) 包含指針的對象:正常分配

[16B, 32KB] : 正常分配

(32KB, -) : 大對象分配其中Tiny分配和大對象分配都屬於內存管理的優化範疇,這裡暫時僅關注一般的分配方法。

以申請size為n的內存為例,分配步驟如下:

Golang內存分配是個相當複雜的過程,其中還摻雜了GC的處理,這裡僅僅對其關鍵數據結構進行了說明,了解其原理而又不至於深陷實現細節。1、Golang程序啟動時申請一大塊內存並劃分成spans、bitmap、arena區域

2、arena區域按頁劃分成一個個小塊。

3、span管理一個或多個頁。

4、mcentral管理多個span供線程申請使用

5、mcache作為線程私有資源,資源來源於mcentral。

Golang 語言深入理解:channel

本文是對 Gopher 2017 中一個非常好的 Talk�: [Understanding Channel](GopherCon 2017: Kavya Joshi – Understanding Channels) 的學習筆記,希望能夠通過對 channel 的關鍵特性的理解,進一步掌握其用法細節以及 Golang 語言設計哲學的管窺蠡測。

channel 是可以讓一個 goroutine 發送特定值到另一個 gouroutine 的通信機制。

原生的 channel 是沒有緩存的(unbuffered channel),可以用於 goroutine 之間實現同步。

關閉後不能再寫入,可以讀取直到 channel 中再沒有數據,並返回元素類型的零值。

gopl/ch3/netcat3

首先從 channel 是怎麼被創建的開始:

在 heap 上分配一個 hchan 類型的對象,並將其初始化,然後返回一個指向這個 hchan 對象的指針。

理解了 channel 的數據結構實現,現在轉到 channel 的兩個最基本方法: sends 和 receivces ,看一下以上的特性是如何體現在 sends 和 receives 中的:

假設發送方先啟動,執行 ch – task0 :

如此為 channel 帶來了 goroutine-safe 的特性。

在這樣的模型里, sender goroutine – channel – receiver goroutine 之間, hchan 是唯一的共享內存,而這個唯一的共享內存又通過 mutex 來確保 goroutine-safe ,所有在隊列中的內容都只是副本。

這便是著名的 golang 並發原則的體現:

發送方 goroutine 會阻塞,暫停,並在收到 receive 後才恢復。

goroutine 是一種 用戶態線程 , 由 Go runtime 創建並管理,而不是操作系統,比起操作系統線程來說,goroutine更加輕量。

Go runtime scheduler 負責將 goroutine 調度到操作系統線程上。

runtime scheduler 怎麼將 goroutine 調度到操作系統線程上?

當阻塞發生時,一次 goroutine 上下文切換的全過程:

然而,被阻塞的 goroutine 怎麼恢復過來?

阻塞發生時,調用 runtime sheduler 執行 gopark 之前,G1 會創建一個 sudog ,並將它存放在 hchan 的 sendq 中。 sudog 中便記錄了即將被阻塞的 goroutine G1 ,以及它要發送的數據元素 task4 等等。

接收方 將通過這個 sudog 來恢復 G1

接收方 G2 接收數據, 並發出一個 receivce ,將 G1 置為 runnable :

同樣的, 接收方 G2 會被阻塞,G2 會創建 sudoq ,存放在 recvq ,基本過程和發送方阻塞一樣。

不同的是,發送方 G1如何恢復接收方 G2,這是一個非常神奇的實現。

理論上可以將 task 入隊,然後恢復 G2, 但恢復 G2後,G2會做什麼呢?

G2會將隊列中的 task 複製出來,放到自己的 memory 中,基於這個思路,G1在這個時候,直接將 task 寫到 G2的 stack memory 中!

這是違反常規的操作,理論上 goroutine 之間的 stack 是相互獨立的,只有在運行時可以執行這樣的操作。

這麼做純粹是出於性能優化的考慮,原來的步驟是:

優化後,相當於減少了 G2 獲取鎖並且執行 memcopy 的性能消耗。

channel 設計背後的思想可以理解為 simplicity 和 performance 之間權衡抉擇,具體如下:

queue with a lock prefered to lock-free implementation:

比起完全 lock-free 的實現,使用鎖的隊列實現更簡單,容易實現

原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/293113.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
小藍的頭像小藍
上一篇 2024-12-25 18:39
下一篇 2024-12-25 18:39

相關推薦

  • 使用Golang調用Python

    在現代軟體開發中,多種編程語言的協作是相當普遍的。其中一種使用場景是Golang調用Python,這使得在使用Python庫的同時,可以利用Golang的高性能和強大並發能力。這篇…

    編程 2025-04-29
  • 使用Golang創建黑色背景圖片的方法

    本文將從多個方面介紹使用Golang創建黑色背景圖片的方法。 一、安裝必要的代碼庫和工具 在開始創建黑色背景圖片之前,我們需要先安裝必要的代碼庫和工具: go get -u git…

    編程 2025-04-29
  • Golang中使用strings.Split函數進行字元串分割的方法

    一、Split函數的基本用法 字元串是編程中常見的數據類型,它們可以在程序中被處理、存儲和傳輸。在Go語言中,字元串也是一個基本的數據類型,而strings包提供了一些操作字元串的…

    編程 2025-04-23
  • Golang環境變數全面解析

    Golang是一門非常流行的開發語言,擁有高效的CGO、簡單易懂的語法、高並發能力等優點,然而它也需要使用環境變數來配置一些參數。在本篇文章中,我們將從多個方面對Golang環境變…

    編程 2025-04-23
  • 深入下探golang http server

    Go語言已經成為了軟體開發領域的熱門語言,它的高性能、應用廣泛、安全性好,使得它成為了眾多開發者心目中的首選編程語言。在眾多應用場景中,golang http server的應用非…

    編程 2025-04-23
  • Compacted:一個高性能的Golang緩存庫

    一、簡介 Compacted是一個使用Golang編寫的緩存庫,旨在提供高性能的內存緩存功能。相對於其他常見的緩存庫,Compacted在內存使用和性能方面都做了一定的優化。 緩存…

    編程 2025-04-23
  • Golang nil解析

    一、什麼是nil Nil是Golang語言中的一個預定義標識符,表示一個零值對象,通常表示一個空指針。Nil被定義為指針類型、函數類型、介面類型、map類型、Slice類型、Cha…

    編程 2025-04-23
  • Golang中文社區介紹

    Go語言或者叫Golang是一個開源項目,目前是由Google開發維護的一種靜態類型、並發安全、編譯型的編程語言。Go語言的特點是結構清晰、並發能力強、具有垃圾回收機制並且支持跨平…

    編程 2025-04-23
  • 詳解golang walk控制項庫

    Golang提供的可視化庫有很多個,其中walk是一個比較好用且強大的庫。本文將從多個方面對walk進行詳細闡述,包括基本控制項、布局、菜單、圖標等方面的內容。 一、控制項基礎 Gol…

    編程 2025-04-22
  • Golang泛型詳解

    Golang泛型成為眾多開發人員關注的話題,因為它使得代碼更加通用、可重用、簡單、易於維護。那麼,什麼是泛型、為什麼它如此重要,如何使用它?本文將從多個方面為您詳細闡述Golang…

    編程 2025-04-20

發表回復

登錄後才能評論