1.併發編程基本概念
併發編程開發將一個過程按照並行算法拆分爲多個可以獨立執行的代碼塊,從而充分利用多核和多處理器提高系統吞吐率
順序、併發與並行
順序是指發起執行的程序只能有一個
併發是指同時發起執行(同時處理)的程序可以有多個(單車道並排只能有一輛車,可同時駛入路段多輛車)
並行是指同時執行(同時做)的程序可以有多個 (多車道並排可以有多個車)
2.例程
Go語言中每個併發執行的單元叫Goroutine,使用go關鍵字後接函數調用來創建一個Goroutine
main函數也是由一個例程來啓動執行,這個例程稱爲主例程,其他例程叫工作例程。主例程結束後工作例程也會隨之銷燬,使用sync.WaitGroup(計數信號量)來維護執行例程執行狀態,可以通過runtime包中的GoSched讓例程主動讓出CPU,也可以通過time.Sleep讓例程休眠從而讓出CPU
package main
import (
"fmt"
"runtime"
"time"
)
func PrintCharts(name string){
for i:= 'A';i <= 'Z';i++{
fmt.Printf("%s %c\n",name,i)
runtime.Gosched()//讓出cpu
}
}
func main() {
//主例程結束後 工作例程自動結束 不管是否運行完畢 所以加延遲時間 等待工作例程結束
//在main函數中是主例程 用go+函數是工作例程
go PrintCharts("1")
go PrintCharts("2")
PrintCharts("3" )
time.Sleep(time.Second *3 )
}
3.閉包陷阱
因爲閉包使用函數外變量,當例程執行是,外部變量已經發生變化,導致打印內容不正確,可使用在創建例程時通過函數傳遞參數(值拷貝)方式避免
package main
import (
"fmt"
"runtime"
"sync"
)
//順序編程可以知道執行順序以及時間
//併發編程無法猜測執行順序
func main() {
group := &sync.WaitGroup{}
nn := 2
group.Add(nn)
for n := 1 ;n <= nn;n++ {
//匿名函數 閉包,閉包指的是函數內調用函數外部的變量
//這樣外層for是在主例程,但是內部的匿名函數是工作歷程,主例程跑的肯定比工作歷程快
//所以當i=3 for跑完了 工作歷程纔開始,爲了避免這樣的情況我們需要把函數內需要的變量 通過參數傳遞進去
//go func() {
// for i := 'A'; i <= 'Z'; i++ {
// fmt.Printf("%v %c\n", n, i)
// runtime.Gosched() //讓出cpu
// }
// group.Done()
//}()
//如下所示
go func(id int) {
for i := 'A'; i <= 'Z'; i++ {
fmt.Printf("%v %c\n",id, i)
runtime.Gosched() //讓出cpu
}
group.Done()
}(n)
}
group.Wait()
}
4.引用類型傳遞
package main
import (
"fmt"
"time"
)
func Copyslice(name []int,names []int) {
for i := 0;i<len(names) && i<len(name);i++{
names[i] = name[i]
}
}
func main() {
name := make([]string,0)
name01 := make([]string,10)
name02 := make(map[int]string)
go func(name,name01 []string,name02 map[int]string) {
time.Sleep(3 * time.Second)
fmt.Println(name)
fmt.Println(name01)
fmt.Println(name02)
}(name,name01,name02)
name = append(name,"aa")
//這裏引用類型的在傳遞過程中
//切片會被影響 但是如果切片是append 會涉及到擴容重新申請內存地址 這樣閉包內 跟外內存地址不同所以無法被影響
//如果是直接改索引並不會重新申請內存地址 這樣就會被影響
//而map不會涉及到重新申請新的內存地址 所以會被影響
name01[0] = "bb"
name02[0] = "cc"
time.Sleep(10*time.Second)
}
5.共享數據
多個例程對同一個內存資源進行修改,未對資源進行同步限制,導致修改數據混亂
互斥鎖
Go語言中sync包中提供了Mutex(互斥鎖),可以用於對資源加鎖和釋放鎖提供對資源同步方式訪問
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
func main() {
//還有一種鎖是讀寫鎖RWMutex
//n個在讀 n個在寫
//Rlock => Rlock 都在讀 不阻塞
//Rlock => Lock 一部分讀 一部分寫 阻塞
//Lock => Rlock 一部分寫 一部分讀 阻塞
//Lock => Lock 都在寫 阻塞
var conter int64
group := &sync.WaitGroup{}
//lock := &sync.Mutex{}
incr := func() {
defer group.Done()
for i:=0;i<100;i++ {
//不用鎖 用atomic也可以 不過針對int64 or int32 而且底層也是使用了鎖
atomic.AddInt64(&conter,1)
//用鎖鎖上 就執行完畢 釋放鎖 //這個叫互斥鎖
//lock.Lock()
//conter++
////如果是針對一個內存地址的話要用同一把鎖 而且切記釋放鎖不然就會成爲死鎖
//lock.Unlock() //釋放鎖
runtime.Gosched()
}
}
decr := func() {
defer group.Done()
for i:=0;i<100;i++ {
atomic.AddInt64(&conter,-1)
//lock.Lock()
//conter--
//lock.Unlock()
runtime.Gosched()
}
}
for i :=0; i<10;i++{
group.Add(2)
go incr()
go decr()
}
group.Wait()
//多個例程對同一快內存空間做修改產生的副作用 需要加鎖去處理
fmt.Println(conter)
}
6.原子
原子操作是指過程不能中斷的操作s,go語言sync/atomic包中提供提供了五類原子操作函數,其操作對象爲整數型或整數指針
a) Add*:增加/減少s
b) Load*:載入
c) Store*:存儲
d) Swap*:更新
e) CompareAndSwap*:比較第一個參數引用指是否與第二個參數值相同,若相同則將第一個參數值更新爲第三個參數
package main
import (
"fmt"
"runtime"
"sync"
)
func PrintCharts(name string,group *sync.WaitGroup){
for i:= 'A';i <= 'Z';i++{
fmt.Printf("%s %c\n",name,i)
runtime.Gosched()
}
group.Done()
}
func main() {
//可以用group來等待例程結束
group := &sync.WaitGroup{}
group.Add(3)
go PrintCharts("1",group)
go PrintCharts("2",group)
PrintCharts("3" ,group)
group.Wait()
}