文章目录

  • ​​go并发编程概念​​
  • ​​goroutine​​
  • ​​channel​​

go并发编程概念

几乎所有的语言都支持并发编程。go也不例外,且轻量级并发编程(协程)是Golang的重要特性。
基本概念

进程:进程是程序在操作系统中的一次执行过程。是系统进行资源分配和调度的基本单位
线程:是进程的一个执行实例,是程序的最小执行单元,是比进程更小的能独立运行的基本单元。一个进程可以创建和销毁多个线程,一个程序至少有一个进程,一个进程至少包含一个线程
并发:多线程程序在单核上运行,就是并发,某一时刻只有一个线程在执行,多个线程轮流执行(每个线程轮流执行的时间很短,宏观上看好像是多个线程同时执行)。
并行:多线程程序也在多核上运行就是并行,某一时刻 有多个线程在执行。

Go线程和协程:一个GO线程上可以起多个协程(go特有的轻量级线程)
协程的特点:有独立的栈空间、共享程序堆空间、调度由用户控制
主线程是一个物理线程,直接作用在CPU上,是重量级的,非常消耗CPU资源。协程是从主线程开启的轻量级线程。一般语言不支持协程线程开多了资源消耗会很大,但是golang协程可以轻松开启数万个。
Golang可以设置程序运行的cpu数(1.8之后默认多核运行)

num := runtime.NumCPU()
runtime.GOMAXPROCS(num)
fmt.Println("程序运行cpu数量:",num)

goroutine

示例代码
启动一个协程 每秒输出一句话,主线程也每秒输出一句话

func test01(){
go sayHello()//启动协程执行逻辑

for i:=0;i<10;i++{
fmt.Println("主线程 ====》",i)
time.Sleep(time.Second)
}
}

//定义一个协程方法
//每秒输出一个 hello world 输出 10次
func sayHello(){
for i:=0;i<10;i++{
fmt.Println("协程 hello world!",i)
time.Sleep(time.Second)
}
}

执行结果,可以看到两个逻辑同时在执行

主线程 ====》 0
协程 hello world! 0
协程 hello world! 1
主线程 ====》 1
主线程 ====》 2
协程 hello world! 2
协程 hello world! 3
主线程 ====》 3
协程 hello world! 4
主线程 ====》 4
主线程 ====》 5
协程 hello world! 5
协程 hello world! 6
主线程 ====》 6
主线程 ====》 7
协程 hello world! 7
协程 hello world! 8
主线程 ====》 8
主线程 ====》 9
协程 hello world! 9

计算 1-100 之间整数的阶加,并将结果输出到 一个map 中

//示例 多协程计算 0-200 每个数字的阶乘,把结果放到map 中
// 执行完成后主线程打印结果
//声明全局变量
//不加锁时 没有出现任何异常
var(
resMap = make(map[int]int,10)
)
func test02(){
for i:=1;i<=100;i++{
go nmuls(i)
}
time.Sleep(time.Second*10)
fmt.Println(resMap)//打印map
//map[85:3656 48:1177 78:3082 35:631 41:862 55:1541 63:2017 66:221...
}
//阶加计算方法
func nmuls(num int)int{
res:=1
for i:=1;i<=num;i++{
res += i
}
resMap[num]=res //将结果放入全局map中
return res
}

channel

不同的协程之间如何通讯
可以使用全局互斥锁 或 使用管道 (管道本身是线程安全的)

声明
var 变量名 chan 数据类型,如:var intChan chan int 声明只放int数据的channel

注意点

  1. channel 的本质就是一个数据结构-队列 ,先进先出。
  2. channel本身是线程安全的
  3. channel是有类型的,只能存放和声明类型相同的数据。
  4. channel 是引用类型,必须初始化(make)后才能写入数据
  5. channel中元素放满后 再放元素就会报dead lock 异常
  6. 没有使用协程的情况下,如果chnnel中数据已经取完了,再取就会报 deadlock
  7. 遍历时如果 channel 没有关闭 则会报 deadlock 异常. 原因应该同6
  8. channel 可以声明成 只读/只写。详见示例

示例代码

func test03(){
var intChan chan int
intChan = make(chan int,3) //创建一个可以存放 3个int的channel
fmt.Printf("intChan 值 =%v 地址是=%p \n",intChan,&intChan)
//intChan 值 =0xc042076080 地址是=0xc04206e018

//插入数据
intChan<- 10
intChan<- 2
intChan<- 13
//fatal error: all goroutines are asleep - deadlock!
//添加的元素超过 容量时 鲍deadlock 错误
//intChan<- 4
//intChan 长度 len= 3 容量= 3
fmt.Printf("intChan 长度 len= %v 容量= %v \n",len(intChan),cap(intChan))

//读取数据
num2:=<-intChan
fmt.Println("num2=",num2)
fmt.Printf("intChan 长度 len= %v 容量= %v \n",len(intChan),cap(intChan))
num3:=<-intChan
num4:=<-intChan
//没有元素的时候获取元素 会报异常 fatal error: all goroutines are asleep - deadlock!
//num4=<-intChan
fmt.Println(num3,num4)


//创建可以放所有类型的 channel
var aChan chan interface{}
aChan = make(chan interface{},5)
aChan<- 1
aChan<- "123"
aChan<- 12.12
o:=<-aChan
fmt.Println("取出元素:",o)//取出元素: 1


//channel 遍历 不能通过普通的for 循环遍历 (数量可能会变化)
//
// for i:=0;i<len(aChan);i++{

// }
//遍历时如果 channel 没有关闭 则会报 deadlock 异常,fatal error: all goroutines are asleep - deadlock!
//如果channel 关闭了,则不会报异常,遍历完 程序退出
fmt.Printf("aChan 长度 len= %v 容量= %v \n",len(aChan),cap(aChan))

close(aChan)
for v:=range aChan{
fmt.Println("v=",v)
}
}

互斥锁实现数据同步示例

//示例 多协程计算 0-200 每个数字的阶乘,把结果放到map 中
// 执行完成后主线程打印结果
//声明全局变量
//不加锁时 没有出现任何异常
var(
resMap = make(map[int]int,10)
lock sync.Mutex //定义全局排他锁
)
func test02(){
for i:=1;i<=20;i++{
go nmuls(i)
}
time.Sleep(time.Second*10)
fmt.Println(resMap)//打印map
//map[85:3656 48:1177 78:3082 35:631 41:862 55:1541 63:2017 66:221
}
//阶加计算方法
func nmuls(num int)int{
res:=1
for i:=1;i<=num;i++{
res += i
}
lock.Lock()//写数据前枷锁
resMap[num]=res //将结果放入全局map中
lock.Unlock()//写数据完成进行解锁
return res
}

Channel示例
统计 1-20000 的数字中,哪些是素数

func test04(){
intChan := make(chan int,1000)
primeChan := make(chan int,2000)
exitChan := make(chan bool,4)
//开启协程 插入数据
go putNum(intChan,100)

//开启四个 计算协程
for i:=1;i<=4;i++{
go primeNum(intChan,primeChan,exitChan)
}

//当 exitchannel中取出四个元素后 证明任务执行完毕
go func(){
for i:=1;i<=4;i++{
<-exitChan
}
close(primeChan)
}()
//遍历 primeChan
fmt.Println("素数共",len(primeChan),"个如下:")
for v:=range primeChan{
fmt.Println(v)
}

}
//放指定数量的数到channel中
func putNum(intChan chan int,num int){
for i:=1;i<=num ;i++{
intChan<-i
}
//添加完了 关闭 channel
close(intChan)
}
//从 channel中取数据并判断是否为素数,如果是 则添加到 素数 channel中
//如果取不到数据了(数据一处理完)则 exitChannel中添加元素并 退出程序
func primeNum(intChan chan int,primeChan chan int,exitChan chan bool){
var flag bool
for{
time.Sleep(time.Millisecond*10)//睡10毫秒
num,ok:=<-intChan
if !ok{
break; //取不到数据 则 退出
}
flag =true
for i:=2;i<num;i++{
if num%i ==0{//证明是不是素数
flag = false
break
}
}
if flag{//如果是素数则加到channel中
primeChan<-num
}

}
fmt.Println("一个协程由于取不到数据而退出")
exitChan<-true//添加标识 到 退出 channel
}

只读只写协程示例

func test05(){
//默认情况下 channel是可读可写
//声明为只写
var chan2 chan<- int
chan2 = make(chan int,3)
chan2<- 1
chan2<- 2
//num:=<-chan2 //error invalid operation:
//声明只读channel
//var chan3 <-chan int
//chan3 = make(chan int,3)
//chan3<- 1 //invalid operation:
//num:=<-chan3
//fmt.Println("num=",num) // deadlock!

//协程测试 只读 和只写
var ch chan int
ch = make(chan int,10)
exitChan := make(chan bool,2)
go send(ch,exitChan)
go recv(ch,exitChan)
// go func(){
// for i:=1;i<=2;i++ {
// val :=<-exitChan
// fmt.Println("执行结束")
// }
// close(exitChan)
// }()
total:=0
for v := range exitChan{
fmt.Println("读取内容v=",v)
total ++
if(total>= 2){
break
}
}

fmt.Println("执行结束")

}
//该方法只写数据到 channel
func send(ch chan<- int,exitChan chan bool){
fmt.Println("send 开始")
for i:=1;i<=10;i++{
ch<-i
}
close(ch)
fmt.Println("send 结束")
exitChan<- true
}
//该方法只读数据
func recv(ch <-chan int,exitChan chan bool){
fmt.Println("recv 开始")
for {
v,ok:=<-ch
if !ok{
fmt.Println("读取值错误",ok)
break
}
fmt.Println("读取到值==》:",v)
}
fmt.Println("recv 结束")
exitChan<-true
}

select解决deadlock问题

func test06(){
//select 解决从 管道 取数据的阻塞问题
intChan :=make(chan int,10)
for i:=0;i<10;i++{
intChan<-i
}

strChan :=make(chan string,5)
for i:=0;i<5;i++{
strChan<-"hello"+fmt.Sprintf("%d",i)
}

//传统方式 遍历管道时 如果 没有关闭会导致阻塞 而报错 deadlock
//实际开发过程中,可能不确实什么时候该关闭管道
//可以使用select 解决报错的问题
for{
select {
//这里 虽然intChan一直没有关闭,但是不会一直阻塞而deadlock,而是自动到写一个case匹配
case v:=<-intChan:
fmt.Printf("从intChan取值 %d \n",v)
time.Sleep(time.Second)
case v:=<-strChan:
fmt.Printf("从strChan取值 %s \n",v)
time.Sleep(time.Second)
default:
fmt.Printf("都没有取到值\n")
return
}
}

}

协程异常处理

func test07(){
go sayHello2()
go testPanic()
for i:=0;i<11;i++{
fmt.Println("main ok~",i)
time.Sleep(time.Second)
}

}
func sayHello2(){
for i:=0;i<10;i++{
time.Sleep(time.Second)
fmt.Println("helloWorld~")
}
}
func testPanic(){
//捕获异常
//如果不捕获这个异常,那么主线程一起其他协程都会因异常而退出执行
//捕获之后,该协程的异常不会影响其他协程和主线程
defer func(){
if err:=recover();err!=nil{
fmt.Println("方法发生错误,",err)
}
}()
var mapp map[int]string
mapp[1]="golang"//没有执行make所以肯定报错
}