文章目錄
1 基本概念
1.1 進程和線程說明
- 1 進程就是程序在操作系統中的一次執行過程,是系統進行資源分配和調度的基本單位
- 2 線程是進程的一個執行實例,是程序執行的最小單元,它是比進程更小的能獨立運行的基本單位
- 3 一個進程可以創建和銷燬多個線程,同一個進程中的多個線程可以併發執行
- 4 一個程序至少有一個進程,一個進程至少有一個線程
1.2 併發和並行
併發和並行(併發包含並行)
- 1)多線程程序在單核上運行,就是併發
因爲在一個CPU上,比如有10個線程,每個線程執行10ms(進行輪詢操作),從人爲角度看,好像是這10個線程都在運行,但是從微觀上看,在某個時間點看,其實只有一個線程在執行,這就是併發。 - 2)多線程程序在多核上運行,就是並行
因爲是在多個CPU上(比如有10個CPU),比如有10個線程,每個線程執行10ms(各自在不同cpu上執行),從人爲角度看,這10個線程都在運行,但是從微觀上看,在某個時間點看,也同時有10個線程在執行,這就是並行
2 goroutine協程
在一個go主線程(也可以理解爲一個進程)之上可以起多個協程,可以理解爲協程是輕量級的線程(編譯器優化)。
Go協程的特點:
- 有獨立的棧空間
- 共享程序的堆空間
- 調度由用戶控制
- 協程是輕量級的線程
MPG
總結:
- 主線程是一個物理線程,其是直接作用在cpu上的,是重量級的,非常消耗cpu資源。
- 協程從主線程開啓的,是輕量級的線程,是邏輯太。對資源消耗相對小。
- Golang的協程機制是重要的特點,可以輕鬆地開啓上萬個協程。
- 如果主線程執行結束退出了,即使協程還沒有執行完畢,也會退出,當然協程也可以在主線程沒有執行結束前就已經結束了,如完成了自己的任務。
2.1 exa1
// goroutine是Go並行設計的核心。
// goroutine說到底其實就是協程,但是它比線程更小,十幾個goroutine可能體現在底層就是五六個線程,Go語言內部幫你實現了這些goroutine之間的內存共享。
// 執行goroutine只需極少的棧內存(大概是4~5KB),當然會根據相應的數據伸縮。
// 也正因爲如此,可同時運行成千上萬個併發任務。goroutine比thread更易用、更高效、更輕便。
// goroutine是通過Go的runtime管理的一個線程管理器。
// goroutine通過go關鍵字實現了,其實就是一個普通的函數。
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 5; i++ {
runtime.Gosched()
fmt.Println(s)
}
}
func main() {
go say("world") //開一個新的Goroutines執行
go say("world") //開一個新的Goroutines執行
say("hello") //當前Goroutines執行
}
2.2 exa2
package main
import (
"fmt"
"strconv"
"time"
)
/*
要求:
1)在主線程(可以理解成進程)中,開啓一個goroutine,該協程每隔1秒輸出“hello, world”
2)在主線程中也每隔一秒輸出“hello, golang”,輸出10此後,退出程序
3)要求主線程和goroutine同時執行
*/
func say() {
for i := 0; i < 10; i++ {
fmt.Println("say() hello, world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go say() // 開啓一個協程
for i := 0; i < 10; i++ {
fmt.Println("main() hello, world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
2.3 exa3
package main
import (
"fmt"
"runtime"
)
func main() {
num := runtime.NumCPU()
fmt.Printf("當前系統CPU數量 = %v", num) // 6
// 設置Golang中使用最多CPU數量
runtime.GOMAXPROCS(num)
}
3 channel管道
channel管道隊列是先進先出的機制,當取完管道的數據後需要有一定機制來判斷此管道爲空,否則會報錯;channel是線程安全的,多個協程操作同一個管道時,不需要加鎖,不會發生資源競爭問題;channel是有類型的,一個string的channel只能存放string類型的數據
channel使用的注意事項
- channel中只能存放指定的數據類型
- channel的數據放滿後,就不能再放入了
- 如果從channel取出數據後,可以繼續放入
- 在沒有使用協程的情況下,如果channel數據取完了,再去取數據,就會報錯 deadlock
PS: 單線程是算好一個寫一個,協程是同步算,一個個寫
3.1 example
3.1.1 exa1
package main
import (
"fmt"
"sync"
"time"
)
// 需求:現在計算1-200的各個數的階乘,並且把各個數的階乘放到map中
// 最後顯示出來。要求使用goroutine完成
var (
myMap = make(map[int]int, 10)
// 聲明一個全局的互斥鎖 lock 是一個全局的互斥鎖(寫數據鎖)
// sync是同步的意思,Mutex是互斥
lock sync.Mutex
)
// 編寫一個函數,來計算各個數的階乘,並放入到一個map中
func cal(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
// 我們將每循環一次的結果放入到myMap中
// 加鎖
lock.Lock()
myMap[n] = res // 如果沒有保護機制會報Fatal錯誤 fatal error: concurrent map writes
// 解鎖
lock.Unlock()
}
func main() {
for i := 1; i <= 20; i++ {
go cal(i)
// 使用goroutine來完成,效率高,但是會出現併發/並行安全的問題
// 這裏就產生了如何讓多個goroutine之間通信的問題?
}
// 解決方法
// 1)全局變量加互斥鎖
// 2)使用channel管道解決
time.Sleep(time.Second * 3)
lock.Lock()
// 遍歷這個map結果
for i, v := range myMap {
fmt.Printf("map[%d] = %d\n", i, v)
}
lock.Unlock()
}
3.1.2 exa2
package main
import (
"fmt"
)
func main() {
// 1.創建一個可以存放3個int類型的管道
var intChan chan int
intChan = make(chan int, 3)
fmt.Printf("inChan 的值=%v intChan本身的地址= %p\n", intChan, &intChan)
// inChan 的值=0xc000088080 intChan本身的地址= 0xc000082018
// 2.向管道寫入數據
// 當我們給管道寫入數據時候,不能超過make的容量
intChan<- 10
num := 211
intChan<- num
intChan<- 59
// 3.看看管道的長度和cap容量 管道不能自動增長
fmt.Printf("Channel的len = %v, cap =%v\n", len(intChan), cap(intChan))
// Channel的len = 2, cap =3
// 4.從管道中讀取數據
var num2 int
num2 = <-intChan
// <-intChan 也可以這樣寫,這樣的話就會把數據丟掉
fmt.Println("num2=", num2)
fmt.Printf("Channel的len = %v, cap =%v\n", len(intChan), cap(intChan))
// 5.在沒有使用協程的情況下,如果我們的管道數據已經全部取出,若再取數據,則會報錯 deadlock
}
3.1.3 exa3(channel不同數據類型的聲明)
package main
import (
"fmt"
)
type Cat struct {
Name string
Age int
}
func main() {
// 1.channel的map存放和讀取
var mapChan chan map[string]string
mapChan = make(chan map[string]string, 10)
m1 := make(map[string]string, 20)
m1["cities1"] = "beijing"
m1["cities2"] = "shanghai"
m2 := make(map[string]string, 20)
m2["hero1"] = "宋江"
m2["hero2"] = "吳淞"
mapChan <- m1
mapChan <- m2
// 2.channel的struct類型
var catChan chan Cat
catChan = make(chan Cat, 10)
cat1 := Cat{
Name: "tom",
Age: 10,
}
cat2 := Cat{
Name: "merry",
Age: 12,
}
catChan <- cat1
catChan <- cat2
//取數據
//cat11 := <-catChan
//cat22 := <-catChan
// 3.channel的指針類型
var cat2Chan chan *Cat
cat2Chan = make(chan *Cat, 10)
cat3 := Cat{
Name: "tom",
Age: 10,
}
cat4 := Cat{
Name: "merry",
Age: 12,
}
cat2Chan <- &cat3
cat2Chan <- &cat4
//取數據
//cat33 := <-cat2Chan
//cat44 := <-cat2Chan
// 4.給 allChan 存放任何類型的數據 定義空接口即可 因任何數據類型都實現了空接口
allChan := make(chan interface{}, 10)
cat5 := Cat{
Name: "jack",
Age: 20,
}
cat6 := Cat{
Name: "jack~~~",
Age: 18,
}
allChan <- 10
allChan <- "rsq"
allChan <- cat5
allChan <- cat6
// 若我們需要獲取到管道中的第三個元素,則先將前兩個數據推出
<-allChan
<-allChan
cat55 := <- allChan
fmt.Printf("cat55的類型=%T cat55=%v\n", cat55, cat55)
// 下邊的寫法是錯誤的,編譯不通過
// fmt.Println("cat222.Name=", cat55.Name)
// 用類型斷言可以取出具體字段
a := cat55.(Cat)
fmt.Println("cat222.Name=", a.Name)
}
3.1.4 exa4
實例:
1)創建一個Person結構體[Name, Age, Address]
2)使用rand方法配合隨機創建10個Person實例,並放入到channel中
3)遍歷channel,將各個Person實例的信息顯示在終端
package main
import (
"fmt"
"math/rand"
"time"
)
type Person struct {
Name string
Age int
Address string
}
func (p Person) Rand() {
rand.Seed(time.Now().UnixNano())
// 定義personChan channel
var personChan chan Person
personChan = make(chan Person, 10)
for i := 0; i < 10; i++ {
p.Name = string(rand.Intn(100))
p.Age = rand.Intn(100)
p.Address = string(rand.Intn(100))
personChan <- p // 寫入數據到管道中
}
// 關閉管道
close(personChan)
for v := range personChan {
fmt.Println(v)
}
}
func main() {
var person Person
person.Rand()
}
3.2 channel的遍歷
使用內置函數 close
可以關閉channel,當channel關閉後,就不能再向channel寫數據了,但是仍然可以從該channel讀取數據。
example.go
package main
import (
"fmt"
)
func main() {
intChan := make(chan int, 3)
intChan <- 100
intChan <- 200
close(intChan) // 關閉管道
// 此時不能夠再往intChan中寫入數據
//intChan <- 300 // 這樣就會報錯:panic: send on closed channel
//fmt.Println("okook")
// 當管道關閉後,讀取數據是可以的
n1 := <-intChan
fmt.Println(n1) // 100
}
channel的遍歷支持for-range,需要注意的是:
1)在遍歷時,如果channel沒有關閉,則會出現deadlock的錯誤
2)在遍歷時,如果channel已經關閉,則會正常遍歷數據,遍歷完後就會退出遍歷
package main
import (
"fmt"
)
func main() {
intChan := make(chan int, 100)
for i := 0; i < 100; i++ {
intChan <- i * 2 // 往管道寫入100個數據
}
close(intChan) // 關閉管道
// 遍歷管道
for v := range intChan {
fmt.Printf("v=%v\n", v)
}
}
4 goroutine和channel結合實例
4.1 exa1
(1) 開啓一個 writeData 協程,向管道 intChan 中寫入50個數據
(2) 開啓一個 readData 協程,從管道 intChan 中讀取 writeData 寫入的數據
(3) 注意: writeData 和 readData 操作的是同一個管道
(4) 主線程需要等待 writeData 和 readData 協程都完成工作才能退出
package main
import (
"fmt"
)
// writeData函數
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
intChan <- i
fmt.Println("writeData ", i)
}
close(intChan)
}
// readData函數
func readData(intChan chan int, exitChan chan bool) {
//for {
// v, ok := <-intChan
// if !ok {
// break
// }
// fmt.Println("readData ", v)
//}
//exitChan <- true
//close(exitChan)
// 從intChan中讀取數據
for num := range intChan {
fmt.Println("readData ", num)
}
exitChan <- true
close(exitChan)
}
func main() {
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)
go writeData(intChan)
go readData(intChan, exitChan)
// 判斷當exitChan管道不爲true,則證明已經取完數據,然後退出主線程即可
// 也可以直接用 <-exitChan 來阻塞管道實現以下的功能
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
4.2 exa2
3.1.1中的實例用channel來實現
package main
import (
"fmt"
)
// 需求:現在計算1-200的各個數的階乘,並且把各個數的階乘放到map中
// 最後顯示出來。要求使用goroutine完成
var (
myMap = make(map[int]int, 10)
)
// 編寫一個函數,來計算各個數的階乘,並放入到一個map中
func cal(resChan chan int, exitChan chan bool) {
res := 1
for i := 1; i <= 20; i++ {
res *= i
resChan <- res
myMap[i] = <- resChan
}
close(resChan)
// 我們將每循環一次的結果放入到myMap中
for i, v := range myMap {
fmt.Printf("map[%d] = %d\n", i, v)
}
exitChan <- true
close(exitChan)
}
func main() {
resChan := make(chan int, 1)
exitChan := make(chan bool, 1)
go cal(resChan, exitChan)
<- exitChan
}
4.3 exa3
要求
(1)啓動一個協程,將1-2000的數放入到一個channel中,如numChan
(2)啓動8個協程,從numChan
取出數(n來定義),計算1+...+n
的值,並存放到resChan中
(3)最後8個協程同時完成工作後,再遍歷resChan
,顯示結果如[res[1] = 1 ... res[10] = 55 ...]
package main
import (
"fmt"
)
func writeData(numChan chan int) {
for i := 1; i <= 2000; i++ {
numChan <- i
}
close(numChan)
}
func readData(numChan chan int, resChan chan int, exitChan chan bool) {
for num := range numChan {
res := 0
for i := 1; i <= num; i++ {
res += i
}
resChan <- res
fmt.Printf("res[%d] = %v\n", num, res)
}
exitChan <- true
close(exitChan)
}
func main() {
numChan := make(chan int, 2000)
resChan := make(chan int, 2000)
exitChan := make(chan bool, 1)
go writeData(numChan)
for i := 1; i <= 8; i++ {
go readData(numChan, resChan, exitChan)
}
<- exitChan
close(resChan)
}