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()
}