> 文章列表 > Golang学习+深入(十三)-goroutine/channal

Golang学习+深入(十三)-goroutine/channal

Golang学习+深入(十三)-goroutine/channal

目录

一、goroutine(协程)

1、goroutine

2、MPG模式(goroutine的调度模型)

3、设置Golang运行的cpu数

4、资源争抢

4.1、全局互斥锁解决资源竞争

二、channel(管道)

1、channel(管道)

2、channel的遍历和关闭

channel的关闭

channel的遍历

channel阻塞

3、管道的使用细节


一、goroutine(协程)

进程和线程说明

  1. 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
  2. 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位。
  3. 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
  4. 一个程序至少有一个进程,一个进程至少有一个线程

并发和并行

  1. 多线程程序在单核上运行,就是并发
  2. 多线程程序在多核上运行,就是并行
  3. 并发: 因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。
  4. 并行: 因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行。

1、goroutine

  1. Go主线程(可理解为进程):一个Go线程上可以起多个协程。协程是轻量级的线程
  2. Go协程的特点
  • 有独立的栈空间
  • 共享程序堆空间
  • 调度由用户控制
  • 协程是轻量级的线程
package main
import ("fmt""strconv""time"
)func test(){for i:=1;i<10;i++{fmt.Println("test()..hello,world!"+strconv.Itoa(i))time.Sleep(time.Second)}
}func main(){go test() //开启了一个协程for i:=1;i<10;i++{fmt.Println("main()..hello,golang"+strconv.Itoa(i))time.Sleep(time.Second)}
}
//=================================
D:\\GO_WORKSPACE\\src\\go_code\\project03\\day06>go run main.go
main()..hello,golang1
test()..hello,world!1
test()..hello,world!2
main()..hello,golang2
main()..hello,golang3
test()..hello,world!3
test()..hello,world!4
main()..hello,golang4
main()..hello,golang5
test()..hello,world!5
test()..hello,world!6
main()..hello,golang6
main()..hello,golang7
test()..hello,world!7
test()..hello,world!8
main()..hello,golang8
main()..hello,golang9
test()..hello,world!9
  1. 主线程是一个物理线程,直接作用在cpu上的,是重量级的,非常耗费cpu资源。
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
  3. Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了。

2、MPG模式(goroutine的调度模型)

  1. M:操作系统的主线程(是物理线程)
  2. P:协程执行需要的上下文
  3. G:协程

当前程序有三个M,如果三个M都在一个cpu运行,就是并发,如果在不同的cpu运行就是并行 M1,M2,M3正在执行一个G,M1的协程队列有三个,M2的协程队列有3个,M3协程队列有2个 Go可以容易的起上万个协程。 其他程序C/java的多线程,往往是内核态的,比较重量级,几千个线程可能耗光cpu

3、设置Golang运行的cpu数

为了充分利用多cpu的优势,在Golang程序中,设置运行的cpu数目。

package main
import ("fmt""runtime"
)func main(){//获取当前系统CPU的数量num := runtime.NumCPU()runtime.GOMAXPROCS(num)//设置cpu数量运行go程序fmt.Println("num=",num)
}

4、资源争抢

引出案例

package main
import ("fmt"
)
//需求:计算1-200的各个数的阶乘,并且把各个数的阶乘放到map中
//最后打印出来。要求使用goroutinevar (mmap =make(map[int]int,10)
)func test(n int){res := 1for i:=1;i<=n;i++{res *=i}mmap[n]=res
}func main(){for i:=1;i<=200;i++{go test(i)}for i,v := range mmap{fmt.Printf("mmap[%d]=%d",i,v)}}
===========================
D:\\GO_WORKSPACE\\src\\go_code\\project03\\day07>go run main.go
fatal error: concurrent map writes
.....

4.1、全局互斥锁解决资源竞争

import "sync"
sync包提供了基本的同步基元,如互斥锁。除了Once和WaitGroup类型,
大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些。
package main
import ("fmt""sync""time"
)
//需求:计算1-200的各个数的阶乘,并且把各个数的阶乘放到map中
//最后打印出来。要求使用goroutine
var (mmap =make(map[int]int,10)//声明一个全局的互斥锁lock sync.Mutex  //Mutex:是互斥
)func test(n int){res := 1for i:=1;i<=n;i++{res *=i}lock.Lock()mmap[n]=reslock.Unlock()
}func main(){for i:=1;i<=200;i++{go test(i)}time.Sleep(10*time.Second)lock.Lock()for i,v := range mmap{fmt.Printf("mmap[%d]=%d\\n",i,v)}lock.Unlock()
}
==================
结果
....
mmap[107]=0
mmap[108]=0
mmap[77]=0
mmap[189]=0
mmap[64]=-9223372036854775808
mmap[74]=0
mmap[46]=1150331055211806720
mmap[152]=0
mmap[109]=0
mmap[149]=0
mmap[183]=0
mmap[197]=0
mmap[37]=1096907932701818880
mmap[105]=0
因为阶乘超过int存储的最大范围所以上面结果是0

还可以使用下面的channel(管道)来解决资源争抢的问题

二、channel(管道)

1、channel(管道)

  1. channel本质就是一个数据结构-队列
  2. 数据是先进先出
  3. 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
  4. channel是有类型的,一个string的channel只能存放string类型数据
基本语法var 变量名 chan 数据类型
例:var intChan chan int //(intChan用于存放int数据)intChan = make(chan int,10) //初始化num:=999intChan<-10  //向channel写入数据intChan<-num //向channel写入数据var perChan chan Person 
说明:
1.channel是引用类型
2.channel必须初始化才能写入数据,即make后才能使用
3.管道是有类型的,intChan只能写入整数int
package main
import ("fmt"
)func main() {var intChan chan int //(intChan用于存放int数据)intChan = make(chan int,3) //初始化fmt.Printf("intChan 的值=%v intChan本身的地址=%p \\n",intChan,&intChan)num:=999intChan <- 10  //向channel写入数据intChan <- num //向channel写入数据,注意:往管道中写入数据,不能超过其容量fmt.Printf("intChan len=%v cap= %v \\n",len(intChan),cap(intChan))var num2 intnum2 = <- intChan//从管道中取出数据,注意,管道中数据全取完,再取就是报错deadlock!fmt.Println(num2)num2 = <- intChanfmt.Println(num2)num2 = <- intChanfmt.Println(num2)fmt.Printf("intChan len=%v cap= %v \\n",len(intChan),cap(intChan))
}
  1. channel中只能存放指定的数据类型
  2. channel的数据存放满后,就不能再放入了
  3. 如果从channel取出数据后,可以继续放入
  4. 在没有使用协程的情况下,如果channel数据取完了,再取,就会报deadlock

2、channel的遍历和关闭

channel的关闭

使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据。

func close(c chan<- Type):内建函数close关闭信道,该通道必须为双向的或只发送的。
========================
package main
import ("fmt"
)func main(){intChan := make(chan int,3)intChan <- 100  intChan <- 200 close(intChan)n1:=<-intChanfmt.Println("n1=",n1)intChan <- 300 fmt.Println("ok")
}
========================
n1= 100
panic: send on closed channel

channel的遍历

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}//遍历时,如果channel没有关闭,会出现deadlock的错误//close(intChan)//关闭后,遍历数据是正常的for v := range intChan{fmt.Println("v=",v)}
}
========================
....
v= 192
v= 194
v= 196
v= 198
fatal error: all goroutines are asleep - deadlock!
....

goroutine/channal结合

package main
import ("fmt"
)func writeData(intChan chan int){for i :=1;i<=50;i++{intChan<- ifmt.Println("writeData=",i)}close(intChan)
}func readData(intChan chan int,exitChan chan bool){for {v,ok := <- intChanif !ok {break}fmt.Println("readData=",v)}exitChan<-trueclose(exitChan)
}func main(){intChan:=make(chan int,50)exitChan:=make(chan bool,1)go writeData(intChan)go readData(intChan,exitChan)for{_,ok := <- exitChanif !ok {break}}
}

channel阻塞

如果编译器(运行),发现一个管道,只有写,而没有读,则该管道,会阻塞。 写管道和读管道的频率不一致,不影响,无所谓。

3、管道的使用细节

  1. channel可以声明为只读,或者只写性质,默认情况下,管道是双向。
package main
import ("fmt"
)func main(){//声明为只写var chan1 chan<- intchan1 =make(chan int,3)chan1<-20fmt.Println("chan1=",chan1)//num := <-chan1 //.\\main.go:13:11: invalid operation: cannot receive from send-only channel chan1 (variable of type chan<- int)//fmt.Println("num=",num)//声明为只读var chan2 <-chan intnum1 := <-chan2fmt.Println("num1=",num1)}
  1. channel只读只写案例
package main
import ("fmt"
)func main(){var ch chan intch = make(chan int,10)exitChan := make(chan struct{},2)go send(ch,exitChan)  //发送go recv(ch,exitChan)  //接收var total =0for _ = range exitChan {total++if total ==2 {break}}fmt.Println("结束...")
}func send(ch chan<- int,exitChan chan struct{}){for i:=0;i<10;i++{ch<-i}close(ch)var a struct{}exitChan <-a
}func recv(ch <-chan int,exitChan chan struct{}){for {v,ok :=<-chif !ok {break}fmt.Println(v)}var a struct{}exitChan <-a
}
  1. 使用select可以解决从管道取数据的阻塞问题 实际开发中,可能不好确定什么时候关闭该管道
package main
import ("fmt"
)func main(){intChan :=make(chan int,10)for i:=0;i<10;i++ {intChan<-i}stringChan := make(chan string,5)for i :=0;i<5;i++{stringChan<-"h" + fmt.Sprintf("%d",i)}label:for {select {//管道intChan一直不关闭,不会导致阻塞而deadlock//会自动到下一个case匹配case v:=<-intChan: fmt.Printf("从intChan读取数据%d\\n",v)case v:=<-stringChan: fmt.Printf("从stringChan读取数据%s\\n",v)default:fmt.Println("退出")break label}}
}
  1. goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题

说明: 如果我们起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic,就会造成整个程序崩溃,这时我们可以在goroutine中使用recover来捕获panic,进行处理,这样即使这个协程发生问题,但是主线程仍然不受影响,可以继续执行。

package main
import ("fmt""time"
)func main(){go test1()go test2()for i:=0;i<10;i++{fmt.Println("main() ..",i)time.Sleep(time.Second)}
}func test1(){for i:=0;i<10;i++{fmt.Println("hello,world!")}
}func test2(){defer func(){if err := recover();err!=nil {fmt.Println("test2() 发生错误",err)}}()var mmap map[int]string mmap[0]="go lang"
}

干我们这行,啥时候懈怠,就意味着长进的停止,长进的停止就意味着被淘汰,只能往前冲,直到凤凰涅槃的一天!