中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

GO的鎖和原子操作實例分析

發布時間:2023-02-24 14:13:43 來源:億速云 閱讀:111 作者:iii 欄目:開發技術

本篇內容介紹了“GO的鎖和原子操作實例分析”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

鎖是什么

鎖 是用于解決隔離性的一種機制

某個協程(線程)在訪問某個資源時先鎖住,防止其它協程的訪問,等訪問完畢解鎖后其他協程再來加鎖進行訪問

在我們生活中,我們應該不會陌生,鎖是這樣的

本意是指置于可啟閉的器物上,以鑰匙或暗碼開啟,引申義是用鎖鎖住、封閉

生活中用到的鎖

上鎖基本是為了防止外人進來、防止自己財物被盜

編程語言中的鎖

鎖的種類更是多種多樣,每種鎖的加鎖開銷以及應用場景也不盡相同

鎖是用來做什么的

用來控制各個協程的同步,防止資源競爭導致錯亂問題

在高并發的場景下,如果選對了合適的鎖,則會大大提高系統的性能,否則性能會降低。

那么知道各種鎖的開銷,以及應用場景很有必要

GO中的鎖有哪些?

  • 互斥鎖

  • 讀寫鎖

我們在編碼中會存在多個 goroutine 協程同時操作一個資源(臨界區),這種情況會發生競態問題(數據競態

舉一個生活中的例子

生活中最明顯的例子就是,大家搶著上廁所,資源有限,只能一個一個的用

舉一個編碼中的例子

package main

import (
	"fmt"
	"sync"
)

// 全局變量
var num int64
var wg sync.WaitGroup

func add() {
	for i := 0; i < 10000000; i++ {
		num = num + 1
	}
	// 協程退出, 記錄 -1
	wg.Done()
}
func main() {
	// 啟動2個協程,記錄 2
	wg.Add(2)

	go add()
	go add()

	// 等待子協程退出
	wg.Wait()
	fmt.Println(num)
}

按照上述代碼,我們的輸出結果應該是 20000000,每一個協程計算 10000000 次,可是實際結果卻是

10378923

每一次計算的結果還不一樣,出現這個問題的原因就是上述提到的資源競爭

兩個 goroutine 協程在訪問和修改num變量,會存在2個協程同時對num+1 , 最終num 總共只加了 1 ,而不是 2

這就導致最后的結果與期待的不符,那么我們如何解決呢?

我們當然是用鎖控制同步了,保證各自協程在操作臨界區資源的時候,先確實是否拿到鎖,只有拿到鎖了才能進行對臨界區資源的修改

先來看看互斥鎖

互斥鎖

GO的鎖和原子操作實例分析

互斥鎖的簡單理解就像上述我們講到上廁所的案例一樣,同一時間點,只能有一個人在使用其他人只能排隊等待

在編程中,引入了對象互斥鎖的概念,來保證共享數據操作的完整性

每個對象都對應于一個可稱為互斥鎖的標記,這個標記用來保證在任一時刻,只能有一個協程訪問該對象。

應用場景

寫大于讀操作的

它代表的資源就是一個,不管是讀者還是寫者,只要誰擁有了它,那么其他人就只有等待解鎖后

我們來使用互斥鎖解決上述的問題

互斥鎖 - 解決問題

互斥鎖是一種常用的控制共享資源訪問的方法,它能夠保證同時只有一個 goroutine 協程可以訪問共享資源

Go 中使用到如下 1個知識點來解決

sync包Mutex類型 來實現互斥鎖

package main

import (
   "fmt"
   "sync"
)

// 全局變量
var num int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
   for i := 0; i < 10000000; i++ {
      // 訪問資源前  加鎖
      lock.Lock()
      num = num + 1
      // 訪問資源后  解鎖
      lock.Unlock()
   }
   // 協程退出, 記錄 -1
   wg.Done()
}
func main() {
   // 啟動2個協程,記錄 2
   wg.Add(2)

   go add()
   go add()

   // 等待子協程退出
   wg.Wait()
   fmt.Println(num)
}

執行上述代碼,我們能看到,輸出的結果與我們預期的一致

20000000

使用互斥鎖能夠保證同一時間有且只有一個goroutine 協程進入臨界區,其他的goroutine則在等待鎖

當互斥鎖釋放后,等待的 goroutine 協程才可以獲取鎖進入臨界區

如何知道哪一個協程是先被喚醒呢?

可是,多個goroutine 協程同時等待一個鎖時,如何知道哪一個協程是先被喚醒呢?

互斥鎖這里的喚醒的策略是隨機的,并不知道到底是先喚醒誰

讀寫鎖

為什么有了互斥鎖 ,還要讀寫鎖呢?

很明顯就是互斥鎖不能滿足所有的應用場景,就催生出了讀寫鎖,我們細細道來

互斥鎖是完全互斥的,不管協程是讀臨界區資源還是寫臨界區資源,都必須要拿到鎖,否則就無法操作(這個限制太死了對嗎)

可是在我們實際的應用場景下是讀多寫少

若我們并發的去讀取一個資源,且不對資源做任何修改的時候如果也要加鎖才能讀取數據,是不是就很沒有必要呢

這種場景下讀寫鎖就發揮作用了,他就相對靈活了,也很好的解決了讀多寫少的場景問題

讀寫鎖的種類

  • 讀鎖

  • 寫鎖

GO的鎖和原子操作實例分析

當一個goroutine 協程獲取讀鎖之后,其他的 goroutine 協程如果是獲取讀鎖會繼續獲得鎖

可如果是獲取寫鎖就必須等待

當一個 goroutine 協程獲取寫鎖之后,其他的goroutine 協程無論是獲取讀鎖還是寫鎖都會等待

我們先來寫一個讀寫鎖的DEMO

Go 中使用到如下 1個知識點來解決

sync包RWMutex類型 來實現讀寫鎖

package main

import (
   "fmt"
   "sync"
   "time"
)

var (
   num    int64
   wg     sync.WaitGroup
   //lock   sync.Mutex
   rwlock sync.RWMutex
)

func write() {
   // 加互斥鎖
   // lock.Lock()

   // 加寫鎖
   rwlock.Lock()

   num = num + 1
   // 模擬真實寫數據消耗的時間
   time.Sleep(10 * time.Millisecond)

   // 解寫鎖
   rwlock.Unlock()

   // 解互斥鎖
   // lock.Unlock()

   // 退出協程前 記錄 -1
   wg.Done()
}

func read() {
   // 加互斥鎖
   // lock.Lock()

   // 加讀鎖
   rwlock.RLock()

   // 模擬真實讀取數據消耗的時間
   time.Sleep(time.Millisecond)

   // 解讀鎖
   rwlock.RUnlock()

   // 解互斥鎖
   // lock.Unlock()

   // 退出協程前 記錄 -1
   wg.Done()
}

func main() {
   // 用于計算時間 消耗
   start := time.Now()

   // 開5個協程用作 寫
   for i := 0; i < 5; i++ {
      wg.Add(1)
      go write()
   }

   // 開500 個協程,用作讀
   for i := 0; i < 1000; i++ {
      wg.Add(1)
      go read()
   }

   // 等待子協程退出
   wg.Wait()
   end := time.Now()

   // 打印程序消耗的時間
   fmt.Println(end.Sub(start))
}

我們開5個協程用于寫,開1000個協程用于讀,使用讀寫鎖加鎖,結果耗時 54.4871ms 如下

54.4871ms

如果我們將上述代碼修改成加 互斥鎖,運行之后的結果是 1.7750029s 如下

1.7750029s

是不是結果相差很大呢,對于不同的場景應用不同的鎖,對于我們的程序性能影響也是很大,當然上述結果,若讀協程,和寫協程的個數差距越大,結果就會越懸殊

我們總結一下這一小塊的邏輯:

GO的鎖和原子操作實例分析

  • 寫者是排他性的,一個讀寫鎖同時只能有一個寫者或多個讀者

  • 不能同時既有讀者又有寫者

  • 如果讀寫鎖當前沒有讀者,也沒有寫者,那么寫者可以立刻獲得讀寫鎖,否則它必須自旋在那里,直到沒有任何寫者或讀者。

  • 如果讀寫鎖沒有寫者,那么讀者可以立即獲得該讀寫鎖,否則讀者必須自旋在那里,直到寫者釋放該讀寫鎖。

上述提了自旋鎖,我們來簡單解釋一下,什么是自旋鎖

自旋鎖是專為防止多處理器并發而引入的一種鎖,它在內核中大量應用于中斷處理等部分(對于單處理器來說,防止中斷處理中的并發可簡單采用關閉中斷的方式,即在標志寄存器中關閉/打開中斷標志位,不需要自旋鎖)。

簡單來說,在并發過程中,若其中一個協程拿不到鎖,他會不停的去嘗試拿鎖,不停的去看能不能拿,而不是阻塞睡眠

自旋鎖和互斥鎖的區別

互斥鎖

當拿不到鎖的時候,會阻塞等待,會睡眠,等待鎖釋放后被喚醒

自旋鎖

當拿不到鎖的時候,會在原地不停的看能不能拿到鎖,所以叫做自旋,他不會阻塞,不會睡眠

如何選擇鎖

對于 C/C++ 而言

  • 若加鎖后的業務操作消耗,大于互斥鎖阻塞后切換上下文的消耗 ,那么就選擇互斥鎖

  • 若加鎖后的業務操作消耗,小于互斥鎖阻塞后切換上下文的消耗,那么選擇自旋鎖

對于 GO 而言

  • 若寫的頻次大大的多余讀的頻次,那么選擇互斥鎖

  • 若讀的頻次大大的多余寫的頻次,那么選擇讀寫鎖

我們都是對自身要求比較高的同學,那么有沒有比鎖還好用的東西呢

自然是有的,我們來看看原子操作

啥是原子操作

"原子操作(atomic operation)是不需要synchronized",這是多線程編程的老生常談了。所謂原子操作是指不會被線程調度機制打斷的操作

這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch (切換到另一個線程)。

原子操作的特性:

原子操作是不可分割的,在執行完畢之前不會被任何其它任務或事件中斷

上述我們的加鎖案例,咱們編碼中的加鎖操作會涉及內核態的上下文切換會比較耗時、代價比較高

針對基本的數據類型我們還可以使用原子操作來保證并發安全

因為原子操作是Go語言提供的方法它在用戶態就可以完成,因此性能比加鎖操作更好

不用我們自己寫匯編,這里 GO 也提供了原子操作的包,供我們一起來使用 sync/atomic

我們對上述的案例做一個延伸

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

var num int64
var l sync.Mutex
var wg sync.WaitGroup

// 普通版加函數
func add() {
	num = num + 1
	wg.Done()
}

// 互斥鎖版加函數
func mutexAdd() {
	l.Lock()
	num = num + 1
	l.Unlock()
	wg.Done()
}

// 原子操作版加函數
func atomicAdd() {
	atomic.AddInt64(&num, 1)
	wg.Done()
}

func main() {
	// 目的是 記錄程序消耗時間
	start := time.Now()
	for i := 0; i < 20000; i++ {

		wg.Add(1)

		// go add()       // 無鎖的  add函數 不是并發安全的
		// go mutexAdd()  // 互斥鎖的 add函數 是并發安全的,因為拿不到互斥鎖會阻塞,所以加鎖性能開銷大

		go atomicAdd()    // 原子操作的 add函數 是并發安全,性能優于加鎖的
	}

	// 等待子協程 退出
	wg.Wait()

	end := time.Now()
	fmt.Println(num)
	// 打印程序消耗時間
	fmt.Println(end.Sub(start))
}

我們使用上述 demo 代碼,模擬了3種情況下,程序的耗時以及計算結果對比

不加鎖

無鎖的 add函數 不是并發安全的

19495
11.9474ms

加互斥鎖

互斥鎖的 add函數 是并發安全的,因為拿不到互斥鎖會阻塞,所以加鎖性能開銷大

20000
14.9586ms

使用原子操作

原子操作的 add函數 是并發安全,性能優于加鎖的

20000
9.9726ms

“GO的鎖和原子操作實例分析”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

go
AI

柘城县| 黑山县| 蒙山县| 林口县| 启东市| 乌兰浩特市| 大石桥市| 二连浩特市| 东乌珠穆沁旗| 罗江县| 静安区| 泽普县| 封丘县| 轮台县| 道孚县| 郯城县| 锦屏县| 忻州市| 巨野县| 营山县| 陇南市| 庆安县| 磐安县| 封开县| 合江县| 富顺县| 岳阳县| 吴川市| 枞阳县| 广水市| 应城市| 肇州县| 朝阳区| 德格县| 鹤壁市| 桐庐县| 彩票| 蓬溪县| 乳山市| 周宁县| 韩城市|