Golang語言goroutine協程併發安全及鎖機制

尹正杰發表於2024-08-04

                                              作者:尹正傑

版權宣告:原創作品,謝絕轉載!否則將追究法律責任。

目錄
  • 一.多協程操作同一資料問題引出
  • 二.互斥鎖Mutex
    • 1 互斥鎖概述
    • 2使用互斥鎖Mutex同步協程
  • 三.讀寫互斥鎖RWMutex
    • 1 讀寫互斥鎖概述
    • 2 讀寫鎖RWMutex引入

一.多協程操作同一資料問題引出

package main

import (
	"fmt"
	"sync"
)

var (
	count int
	wg    sync.WaitGroup
)

func add() {
	defer wg.Done()

	for i := 1; i <= 1000000; i++ {
		count++
	}
}

func sub() {
	defer wg.Done()

	for i := 1; i <= 1000000; i++ {
		count--
	}
}

func main() {
	// 開啟2個協程等待
	wg.Add(2)

	go add()
	go sub()

	wg.Wait()

	// 在理論上,這個count結果應該是0,無論協程怎麼交替執行,最終咱們想象的結果就是0,但事實上並不是!!!
	// 那為什麼結果不為0呢?可以參考上圖的流程,最終值不為0的執行思路。
	fmt.Printf("count = %d\n", count)
}

二.互斥鎖Mutex

1 互斥鎖概述

方法名 功能
func (m *Mutex) Lock() 獲取互斥鎖
func (m *Mutex) Unlock() 釋放互斥鎖
互斥鎖是一種常用的控制共享資源訪問的方法,它能夠保證同一時間只有一個goroutine可以訪問共享資源。

Go語言中使用sync包中提供的Mutex型別來實現互斥鎖。

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

當互斥鎖釋放後,等待的goroutine才可以獲取鎖進入臨界區,多個goroutine同時等待一個鎖時,喚醒的策略是隨機的。

2使用互斥鎖Mutex同步協程

package main

import (
	"fmt"
	"sync"
)

var (
	count int
	wg    sync.WaitGroup
	/*
	互斥鎖:
		"sync.Mutex"為互斥鎖,"Lock()"進行加鎖,"Unlock()"進行解鎖。
		
		使用"Lock()"加鎖後,便不能再次對其進行加鎖,直到利用Unlock()解鎖對其解鎖後,才能再次加鎖。
		
		互斥鎖適用於讀寫不確定場景,即讀寫次數沒有明顯的區別,效能相對來說比較低(因為同一個時刻僅有一個協程可以操作)。
	*/
	lock sync.Mutex
)

func add() {
	defer wg.Done()

	for i := 1; i <= 1000000; i++ {
		// 加鎖,確保一個協程在執行邏輯的時候另外的協程不被執行。
		lock.Lock()
		count++
		// 解鎖
		lock.Unlock()
	}
}

func sub() {
	defer wg.Done()

	for i := 1; i <= 1000000; i++ {
		// 加鎖
		lock.Lock()
		count--
		// 解鎖
		lock.Unlock()
	}
}

func main() {
	// 開啟2個協程等待
	wg.Add(2)

	go add()
	go sub()

	wg.Wait()

	// 使用互斥鎖同步協程,從而解決多協程在對同一個資源處理時資料同步的問題。
	fmt.Printf("count = %d\n", count)
}

三.讀寫互斥鎖RWMutex

1 讀寫互斥鎖概述

方法名 功能
func (rw *RWMutex) Lock() 獲取寫鎖
func (rw *RWMutex) Unlock() 釋放寫鎖
func (rw *RWMutex) RLock() 獲取讀鎖
func (rw *RWMutex) RUnlock() 釋放讀鎖
func (rw *RWMutex) RLocker() Locker 返回一個實現Locker介面的讀寫鎖
互斥鎖是完全互斥的,但是實際上有很多場景是讀多寫少的,當我們併發的去讀取一個資源而不涉及資源修改的時候是沒有必要加互斥鎖的。

這種場景下使用讀寫鎖是更好的一種選擇。讀寫鎖在Go語言中使用sync包中的RWMutex型別。

sync.RWMutex提供瞭如上表所示的5個方法。讀寫鎖分為兩種:讀鎖和寫鎖。

當一個goroutine獲取到讀鎖之後,其他的goroutine如果是獲取讀鎖會繼續獲得鎖,如果是獲取寫鎖就會等待。

而當一個goroutine獲取寫鎖之後,其他的goroutine無論是獲取讀鎖還是寫鎖都會等待。

2 讀寫鎖RWMutex引入

package main

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

var (
	/*
		讀寫鎖:
			RWMutex是一個讀寫鎖,其經常用於讀次數遠遠多於寫次數的場景。

			讀寫鎖的好處是多個協程同時在讀的時候,資料之間不產生影響,鎖不產生影響。

			但多個協程出現了同時讀和寫的情況下才會產生影響,鎖就會產生影響。
	*/
	lock sync.RWMutex

	wg sync.WaitGroup
)

func read(id int) {
	defer wg.Done()

	// 加讀鎖,如果只是讀資料,那麼這個鎖不產生影響,但是讀寫同時發生的時候,就會有影響
	lock.RLock()

	fmt.Printf("開始讀取資料...ID = %d\n", id)
	time.Sleep(time.Second * 5)
	fmt.Printf("讀取資料完成...ID = %d\n", id)

	// 解讀鎖
	lock.RUnlock()

}

func write() {
	defer wg.Done()

	lock.Lock()
	fmt.Printf("開始修改資料...\n")
	time.Sleep(time.Second * 3)
	fmt.Printf("修改資料完成...\n")
	lock.Unlock()

}

func main() {
	wg.Add(11)

	// 啟動協程: 模擬"讀多寫少"場景,10個執行緒讀(無視鎖),總耗時僅需5秒。
	for i := 1; i <= 10; i++ {
		go read(i)
	}

	go write()

	wg.Wait()

	fmt.Println("main程式執行完成,程式已退出!")
}

相關文章