Golang並發(Go程、管道)


基礎

  • 並發:電腦同時聽歌,看小說,打游戲。cpu根據時間片進行划分,交替執行這三個程序。我們可以感覺是同時產生的。
  • 並行:多個cpu(多核)上述動作同時執行
  • C語言:,實現並發過程使用的是多線程(C++的最小資源單元)
  • Golang:Golang中不是線程,而是Go程(goroutine),Go程是Golang原生支持的,每一個Go程占用的系統資源,遠遠小於線程,一個Go程大約需要4k到5k的內存資源,一個程序可以啟動大量的Go程序。相同的資源下,線程啟動幾十個,那么Go程是可以啟動成百上千個。Go程對於高並發,性能非常好
  • Golang啟動Go程,只需要在函數前面加上關鍵字go即可
  • 啟動多個子Go程它們會競爭cpu資源
package main

import (
	"fmt"
	"time"
)

func main() {
	go func() {
		count := 1
		for {
			fmt.Println("======>我是子go程:", count)
			count++
			time.Sleep(time.Second)
		}
	}()

	count := 1
	for {
		fmt.Println("我是主go程:", count)
		count++
		time.Sleep(time.Second)
	}
}

image

return、exit、goexit區別

  • return:返回當前函數
  • exit:退出當前進程
  • goexit:提前退出當前go程

return

package main

import (
	"fmt"
	"time"
)

func main() {
	go func() {
		func(){
			fmt.Println("這是子go程的內部函數")
			return	// 只是返回當前函數,對於上一層的函數會繼續執行
		}()
		fmt.Println("子go程結束")
	}()

	fmt.Println("這是主go程")
	time.Sleep(5 * time.Second)
	fmt.Println("over")
}

image

exit

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	go func() {
		func(){
			fmt.Println("這是子go程的內部函數")
			os.Exit(-1)		// 退出進程
		}()
		fmt.Println("子go程結束")
	}()

	fmt.Println("這是主go程")
	time.Sleep(5 * time.Second)
	fmt.Println("over")
}

image

goexit

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	go func() {
		func(){
			fmt.Println("這是子go程的內部函數")
			runtime.Goexit()	// 退出當前go程
		}()
		fmt.Println("子go程結束")
	}()

	fmt.Println("這是主go程")
	time.Sleep(5 * time.Second)
	fmt.Println("over")
}

image

多go程通信(channel)

  • 但涉及到多go程時,c語言使用互斥量,上鎖來保持資源同步,避免資源競爭問題
  • Golang也支持這種方式,但是go語言更好的解決方案是使用管道、通道
  • 使用通道不需要我們去進行加解鎖
  • A往通道里面寫數據,B從管道里面讀數據,Golang自動幫我們做好了數據同步
package main

import "fmt"

func main() {
	// 使用一個管道,一定要make,同map一樣,否則為nil
	// 此時是無緩沖的管道,會寫一個讀一個,也稱無緩存管道
	numChan := make(chan int)
	// 如果創建的管道給入容量,那么將會批量寫入,也稱有緩沖通道
	// numChan := make(chan int, 10)

	// 創建兩個go程,父母寫數據,兒子讀數據。
	// 這是go程“兒子”,從管道中讀取數據
	go func() {
		for i := 0; i < 50; i++ {
			data := <-numChan
			fmt.Println("<----這是go程“兒子”,讀取數據:", data)
		}
	}()

	go func() {
		for i := 0; i < 20; i++ {
			// 這是go程“媽媽”,寫入20個數據
			numChan <- i
			fmt.Println("---->這是go“媽媽”,寫入數據:", i)
		}
	}()

	for i := 20; i < 50; i++ {
		// 這是go程“爸爸”,寫入30個數據
		numChan <- i
		fmt.Println("---->這是go程”爸爸“,寫入數據:", i)
	}
}

image

管道的注意點

管道nil

如果管道沒有使用make分配空間,那么管道默認為nil,讀取寫入都會阻塞

package main

import "fmt"

func main() {
	var numChan chan int
	numChan <- 1
	fmt.Println("numChan", <- numChan)
}

image

管道死鎖

當管道讀寫次數不一致的時候,如果阻塞在主go程,那么程序會崩潰,如果阻塞在子go程,那么會出現內存泄露

package main

import "fmt"

func main() {
	numChan := make(chan int, 10)
	
	// 寫入數據到管道
	go func() {
		for i := 0; i < 50; i++ {
			numChan <- i
			fmt.Println("寫入數據:", i)
		}
	}()
    
    // 讀,當主程序被管道阻塞時,那么程序將鎖死崩潰
	for i := 0; i < 60; i++ {
		fmt.Println("numChan:", <-numChan)
	}
}

image

for range遍歷管道

for range是不知道管道是否寫完,所以會一直等待,一直等待就會導致死鎖

package main

import "fmt"

func main() {
	numChan := make(chan int, 10)
	go func() {
		for i := 0; i < 50; i++ {
			numChan <- i
			fmt.Println("寫入數據:", i)
		}
	}()

	// 遍歷管道時,只返回值,不返回坐標
	for val := range numChan{
		fmt.Println("讀取數據:",val)
	}
}

image

在寫入端,將管道關閉,for range遍歷關閉管道(nil)時,會退出就不會導致死鎖

package main

import "fmt"

func main() {
	numChan := make(chan int, 10)
	go func() {
		for i := 0; i < 50; i++ {
			numChan <- i
			fmt.Println("寫入數據:", i)
		}
		// 手動關閉管道
		close(numChan)
	}()

	for val := range numChan{
		fmt.Println("讀取數據:",val)
	}
}

image

判斷管道是否已經關閉

我們如何知道一個管道的狀態,如果已經關閉了,讀沒事,會返回零值,如果再寫入的話會有崩潰風險
有沒有類似於map讀取的那種方式val, ok := numMap[0]的這種ok-idiom方式知道呢

package main

import "fmt"

func main() {
	numChan := make(chan int, 10)
	go func() {
		for i := 0; i < 10; i++ {
			numChan <- i
			fmt.Println("寫入數據:", i)
		}
		// 手動關閉管道
		close(numChan)
	}()

	for {
		val, ok := <-numChan
		if ok {
			fmt.Println("讀取數據:", val)
		} else {
			fmt.Println("管道已經關閉")
			break
		}
	}
}

image

單向通道

  • numChan := make(chan int, 10)雙向通道,既可以讀,也可以寫
  • 單向通道:這樣的設計是為了明確語義,一般用於函數參數
    • 單向讀通道:var numChanReadOnly <- chan int
    • 單向寫通道:var numChanWriteOnly chan <- int
  • 雙向管道可以賦值給同類型的單向管道,但單向通道不能賦值給同類型的雙向通道
package main

import (
	"fmt"
	"time"
)

func main() {
	// 生產者消費者模型(、producer)
	// C語言:數組+鎖,thread1:寫,thread2:讀
	// Golang:goroutine + channel

	// 在主函數中創建一個雙向通道
	numChan := make(chan int, 10)
	// 雙向管道可以賦值給同類型的單向管道

	// 將numChan,傳遞給producer,負責生產
	go producer(numChan)

	// 將numChan,傳遞給consumer 負責消費
	go consumer(numChan)

	time.Sleep(5 * time.Second)

}

// 生產者,提供一個只寫通道
func producer(write chan<- int) {
	for i := 0; i < 50; i++ {
		write <- i
		// 寫管道中不允許讀操作
		// data <- write
		fmt.Println("向管道中寫入數據:", i)
	}
}

// 消費者,提供一個只讀通道
func consumer(read <-chan int) {
	// 讀通道不允許寫入操作
	// read <- 12
	for val := range read {
		fmt.Println("向管道中寫入數據:", val)
	}
}

image

管道監聽(select)

當程序中有多個channel協同工作,chan1,chan2,某一時刻,chan1chan2觸發了,程序要做出處理,使用select來監聽多個通道,當管道被觸發時(寫入數據、讀取數據、關閉管道),select語法與switch case很像,但是所有的分支條件必須是管道io

package main

import (
	"fmt"
	"time"
)

func main() {
	// 啟動一個go程,負責監聽兩個channel

	chan1 := make(chan int)
	chan2 := make(chan int)

	go func() {
		for {
			select {
			case val := <-chan1:
				fmt.Println("從chan1讀取數據成功:", val)
			case val2 := <-chan2:
				fmt.Println("從chan2讀取數據成功:", val2)
			}
		}
	}()

	go func() {
		for i := 0; i < 10; i++ {
			chan1 <- i
			time.Sleep(time.Second)
		}
	}()

	go func() {
		for i := 0; i < 10; i++ {
			chan2 <- i
			time.Sleep(time.Second)
		}
	}()

	for{

	}
}

image

管道總結

  • 當管道寫滿了,寫阻塞
  • 當緩沖區讀完了,讀阻塞
  • 如果管道沒有使用make分配空間,管道默認nil
    • 從nil管道讀取/寫入數據,都會阻塞(不會崩潰)
  • 從一個已經close的管道讀取/寫入數據時,會返回零值(不會崩潰)
  • 一個管道,如果重復關閉,程序會崩潰
  • 關閉管道的動作,一定要在寫管道的操作方執行,不應該放在讀端,否則繼續寫會崩潰
  • 讀寫通道次數一定要對等
    • 否則在多個go程中,會出現資源泄露
    • 在主go程中,會出現程序崩潰(deadlock)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM