我的Go併發之旅、02 基本併發原語

来源:https://www.cnblogs.com/linxiaoxu/archive/2022/09/18/16705872.html
-Advertisement-
Play Games

**註:**本文所有函數名為中文名,並不符合代碼規範,僅供讀者理解參考。 Goroutine Go程不是OS線程,也不是綠色線程(語言運行時管理的線程),而是更高級別的抽象,一種特殊的協程。是一種非搶占式的簡單併發子goroutine(函數、閉包、方法)。不能被中斷,但有多個point可以暫停或重新 ...


註:本文所有函數名為中文名,並不符合代碼規範,僅供讀者理解參考。

Goroutine

Go程不是OS線程,也不是綠色線程(語言運行時管理的線程),而是更高級別的抽象,一種特殊的協程。是一種非搶占式的簡單併發子goroutine(函數、閉包、方法)。不能被中斷,但有多個point可以暫停或重新進入。

goroutine 在它們所創建的相同地址空間內執行,特別是在迴圈創建go程的時候,推薦將變數顯式映射到閉包(引用外部作用域變數的函數)中。

fork-join 併發模型

image-20220918185937609

Fork 在程式中的任意節點,子節支可以與父節點同時運行。join 在將來某個時候這些併發分支會合併在一起,這是保持程式正確性和消除競爭條件的關鍵Go語言遵循 fork-join併發模型。

使用 go func 其實就是在創建 fork point,為了創建 join point,我們需要解決競爭條件

sync.WaitGroup

func 競爭條件_解決() {
	var wg sync.WaitGroup
	var data int
	wg.Add(1)
	go func() {
		defer wg.Done()
		data++
	}()
	wg.Wait()
	if data == 0 {
		fmt.Println("Value", data)
	} else {
		fmt.Println("Value 不是 0")
	}
}

通過 sync.WaitGroup 我們阻塞 main 直到 go 程退出後再讓 main 繼續執行,實現了 join point。可以理解為併發-安全計數器,經常配合迴圈使用。

這是一個同步訪問共用記憶體的例子。使用前提是你不關心併發操作的結果,或者你有其他方法來收集它們的結果。

wg.Add(1) 是在幫助跟蹤的goroutine之外完成的,如果放在匿名函數內部,會產生競爭條件。因為你不知道go程什麼時候被調度。

sync.Mutex 互斥鎖

type state struct {
	lock  sync.Mutex
	count int
}

func 結構體修改狀態_互斥鎖() {
	s := state{}
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			// s.lock.Lock()
			defer wg.Done()
			// defer s.lock.Unlock()
			s.count++
		}()
	}
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			// s.lock.Lock()
			defer wg.Done()
			// defer s.lock.Unlock()
			s.count--
		}()
	}
	wg.Wait()
	fmt.Println(s.count)
}

沒有互斥鎖的時候,會導致發生競爭現象,取消互斥鎖的註釋,最終結果為理想的0。

進入和退出一個臨界區是有消耗的,所以一般人會儘量減少在臨界區的時間。

sync.RWMutex 讀寫鎖

本質和普通的互斥鎖相同,但是可以保證在未鎖的情況允許多個讀消費者持有一個讀鎖,在讀消費者非常多的情況下可以提高性能。

在多個讀消費者的情況下,通常使用 RWMutex ,讀消費者較少時,Mutex和RWMutex兩者都可用。

Cond 同步多個go程

cond : 一個goroutine的集合點,等待或發佈一個event。

多個go程暫停在某個point上,等待一個事件信號再繼續執行。沒有cond的時候是怎麼做的,當然是for迴圈,但是這有個大問題。

func 無cond() {
	isOK := false
	go func() {
		for isOK == false {
			// time.Sleep(time.Microsecond) // bad method
			// do something
		}
		fmt.Println("OK I finished")
	}()
	go func() {
		for isOK == false {
			// time.Sleep(time.Microsecond) // bad method
			// do something
		}
		fmt.Println("OK I finished")
	}()
	time.Sleep(time.Second * 5)
	isOK = true
	select {}
}

image-20220918193642390

這會消耗一整個CPU核心的所有周期,有些人會引入 time.Sleep 實際上這會讓演算法低效,這時候我們可以使用 cond。

func 有cond() {
	var wg sync.WaitGroup
	cond := sync.NewCond(&sync.Mutex{})
	test := func() {
		defer wg.Done()
		defer cond.L.Unlock()
		cond.L.Lock()
		cond.Wait()
		fmt.Println("something work...OK finished")
	}
	wg.Add(2)
	go test()
	go test()
	time.Sleep(time.Second * 5)
	cond.Broadcast() // 通知所有go程
	// cond.Signal() // 通知等待時間最久的一個go程
	wg.Wait()
}

cond運行時內部維護一個FIFO列表。與利用channel相比,cond類型性能要高很多。

Once 只允許一次

可以配合單例模式使用,將判斷對象是否為null改為sync.Once用於創建唯一對象。

sync.Once只計算調用Do方法的次數,而不是多少次唯一調用Do方法。所以在必要情況下聲明多個sync.Once變數而不是用一個。下麵的例子輸出 1

func 只調用一次() {
	var once sync.Once
	count := 0
	once.Do(func() {
		count++
	})
	once.Do(func() {
		count--
	})
	fmt.Println(count)
}

Pool 池子

對象池模式是一種創建和提供可供使用的固定數量實例或Pool實例的方法。通常用於約束創建昂貴的場景,比如資料庫連接,以便只創建固定數量的實例,但不確定數量的操作仍然可以請求訪問這些場景。

使用pool的另一個原因是實例化的對象會被GC自動清理,而pool不會

  • 可以通過限制創建的對象數量來節省主機記憶體。
  • 提前載入獲取引用到另一個對象所需的時間,比如建立伺服器連接。

你的併發進程需要請求一個對象,但是在實例化之後很快地處理它們,或者在這些對象的構造可能會對記憶體產生負面影響,這時最好使用Pool設計模式。但是必須確保pool中對象是同質的,否則性能大打折扣。

註意事項

  • 實例化 sync.Pool ,調用 New 方法創建成員變數是線程安全的。
  • 收到來自Get的實例,不要對所接受的對象的狀態做出任何假設。(同質,不需要做if判斷)
  • 當你用完了一個從Pool取出的對象時,一定要調用put,否則無法復用這個實例。通常情況下用defer完成。
  • Pool內的分佈必須大致均勻
type conn struct{}

func 對象池() {
	pool := &sync.Pool{New: func() any {
		time.Sleep(time.Millisecond * 250)
		fmt.Println("創建連接對象")
		return &conn{}
	}}
	for i := 0; i < 10; i++ {
		pool.Put(pool.New())
	}
	fmt.Println("初始化結束")
	c1 := pool.Get()
	c2 := pool.Get()
	pool.Put(c1)
	pool.Put(c2)
}

Channel 通道

channel也可以用來同步記憶體訪問,但最好用於在goroutine之間傳遞消息(channel是將goroutine綁定在一起的粘合劑)。雙向 chan 變數名尾碼加 Stream

帶緩存的channel和不帶緩存的channel聲明是一樣的

var dataStream chan interface{}

雙向channel可以隱式轉換成單向channel,這對函數返回單向通道很有用

var receiveChan <-chan interface{}
var sendChan chan<- interface{}
dataStream := make(chan interface{})

receiveChan = dataStream
sendChan = datraStream

go語言中channel是阻塞的,意味著channel內的數據被消費後,新的數據才可以寫入。通過 <- 操作符的接受形式可以選擇返回兩個值。

salutation,ok := <-dataStream

當channel未關閉時,ok返回true,關閉後返回false。即使channel關閉了,也能讀取到預設值,為了支持一個channel有單個上游寫入,有多個下游讀取。


模擬之前WaitGroup的例子

func 競爭條件_通道() {
	var data int
	var Stream chan interface{} = make(chan interface{})
	go func() {
		data++
		Stream <- struct{}{}
	}()
	<-Stream
	if data == 0 {
		fmt.Println("Value", data)
	} else {
		fmt.Println("Value 不是 0")
	}
}

模擬之前cond同步多個go程的例子

func channel代替cond() {
	var wg sync.WaitGroup
	Stream := make(chan interface{})
	test := func() {
		defer wg.Done()
		<-Stream
		fmt.Println("something work...OK finished")
	}
	wg.Add(1)
	go test()
	go test()
	time.Sleep(time.Second * 5)
	close(Stream)
	wg.Wait()
}

在同一時間打開或關閉多個goroutine可以考慮用channel。


channel操作結果

操作 Channel狀態 結果
Read nil 阻塞
打開且非空 輸出值
打開但空 阻塞
關閉的 預設值,false
只寫 編譯錯誤
Write nil 阻塞
打開但填滿 阻塞
打開但不滿 寫入
關閉的 panic
只讀 編譯錯誤
close nil panic
打開且非空 關閉Channel;仍然能讀取通道數據,直到讀取完畢返回預設值
打開但空 關閉Channel;返回預設值
關閉的 panic
只讀 編譯錯誤

Channel 使用哲學

在正確的環境中配置Channel,分配channel的所有權這裡的所有權被定義為 實例化、寫入和關閉channel的goroutine。重要的是弄清楚哪個goroutine擁有channel。

單向channel聲明的是一種工具,允許我們區分所有者和使用者。一旦我們將channel所有者和非channel所有者區分開來,前面的表的結果會非常清晰。可以開始講責任分配給哪些擁有channel的goroutine和不擁有channel的goroutine。

擁有channel的goroutine

  • 實例化channel
  • 執行寫操作,或將所有權傳遞個另一個goroutine
  • 關閉channel
  • 執行這三件事,並通過只讀channel把它們暴露出來。

使用channel的goroutine

  • 知道channel是何時關閉的 => 檢查第二個返回值
  • 正確處理阻塞 =>取決於你的演算法

儘量保持channel的所有權很小,消費者函數只能執行channel的讀取方法,因此只需要知道它應該如何處理阻塞和channel的關閉。

func 通道使用哲學() {
    // 所有權範圍足夠小,職責明確
	chanOwner := func() <-chan int {
		resultStream := make(chan int, 5)
		go func() { 
			defer close(resultStream)
			for i := 0; i < 5; i++ {
				resultStream <- i
			}
		}()
		return resultStream // 傳遞單向通道給另一個 goroutine
	}
	resultStream := chanOwner()
	for result := range resultStream {
		fmt.Println(result)
	}
	fmt.Println("Done")
}

Select 選擇語句

Go語言運行時將在一組case語句中執行偽隨機選擇。

var c<-chan int // 註意是 nil,永遠阻塞
select{
	case <-c:
    case <- time.After(1 * time.Second):
    fmt.Println("Timed out.")
}

time.After函數通過傳入time.Duration參數返回一個數值並寫入channel。select允許加default語句,通常配合for-select迴圈一起使用,允許go程在等待另一個go程結果的同時,自己乾一些事情。

GOMAXPROCS

通過修改 runtime.GOMAXPROCS 允許你修改OS線程的數量。一般是為了調試,添加OS線程來更頻繁觸發競爭條件。

參考資料

  • 《Go語言併發之道》Katherine CoxBuday

  • 《Go語言核心編程》李文塔

  • 《Go語言高級編程》柴樹彬、曹春輝


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 我的設計模式之旅,本節學習原型模式。從複製原有對象出現的兩大問題思考原型模式存在的必要性。探討原型模式的實現方法。 ...
  • 在寫開源項目的時候,想到了要支持多種redis部署方式,於是對於這塊的生產環境的架構選型展開調研。 推薦使用更新的引擎版本以支持更多的特性, Redis 6.0新特性說明 模塊系統新增多個API。 支持SSL/TLS加密。 支持新的Redis協議:RESP3。 服務端支持多模式的客... ...
  • 我的設計模式之旅。本節詳細說明單例模式的實現方式、優缺點,簡要描述多線程情況下利用雙重鎖定保護單例對象和C#靜態初始化的方式。並用 Golang 實現單例模式,三個工作者需要各自找到電梯搭乘,只有一個電梯!補充C#單線程單例模式的實現。 ...
  • 目錄 一.OpenGL 圖像反色 1.原始圖片 2.效果演示 二.OpenGL 圖像反色源碼下載 三.猜你喜歡 零基礎 OpenGL ES 學習路線推薦 : OpenGL ES 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL ES 學習路線推薦 : OpenGL ES 學習目錄 >> ...
  • 我的博客 俗話說,工欲善其事必先利其器,所以在使用日期前要先對日期進行處理,所以時間戳和字元串的來回來去轉換這個事肯定是要搞的 這次的函數有一個?有兩個?有三個?有四個!上代碼! 哈哈,像不像直播帶貨 本次用到3個內置包 import reimport timeimport calendar 第一個 ...
  • 1. auth模塊 在創建完django項目之後,執行資料庫遷移之後,資料庫里會增加很多新表,其中有一張名為auth_user的表,當訪問django自帶的路由admin的時候,需要輸入用戶名和密碼,其參照的就是auth_user表 使用python3 manage.py crataesupperu ...
  • 在Spring的簡介中我們知道了Spring的核心是控制反轉(ICO)和麵向切麵編程(AOP),我們不直接對ICO進行學習,而是先學習ICO的理論推導。 這是我一個maven項目的結構。 UserDao: package com.jms.dao; public interface UserDao { ...
  • 操作步驟 先設置輸入路徑與輸出路徑 輸入路徑:需要被轉換的文件路徑 輸出路徑:轉換後的文件儲存路徑 我沒有寫這個屬性的交互操作,只是在第一行用字面量進行設置 如果輸出路徑的目錄不存在,則就會進行交互,是否創建該目錄,如果不創建就退出程式 再是選擇字元集轉換的類型,是全部文件預設使用同一套字元集轉換, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...