Go语言关于 goroutine、channel
2020-11-02 15:14
1156 查看
Go--关于 goroutine、channel
goroutine
协程是一种轻量化的线程,由
Go编译器进行优化。
Go协程具有以下特点:
- 有独立的栈空间
- 共享程序堆中的空间
- 调度由用户控制
如果主线程
main函数(主
goroutine或者
main goroutine)返回或者退出时,即使所有协程(
goroutine)还没执行完毕,也会退出。当然,协程可以在主线程未退出之前自己执行完毕,并退出。
- 主线程是一个物理线程,直接作用在
cpu
上。是重量级的,非常耗费cpu
资源。 - 协程从主线程开启的,是轻量级的线程,是逻辑态的。对资源要求相对较小。
Golang
可以开启成千上万个协程。这是Golang
的并发优势。
MPG模式
Go
1.8后,默认让程序运行在多个核上,可以不用设置了Go
1.8前,还是要设置一下,可以更高效的利益cpu
numsCPU :=runtime.NumCPU() //获取系统CPU数 runtime.GOMAXPROCS(numsCPU) //设置运行的CPU数目
channel
在此之前,先说明一种实现同步的方式:加锁(注意这里说的指互斥锁)
需求:计算
n!:
var lock sync.Mutex //使用全局变量加锁 func testInput(n int) { res := 1 for i := 1; i <= n; i++ { res *= i } lock.Lock() myMap = uint64(res) lock.Unlock() } func main() { for i := 1; i <= 50; i++ { go testInput(i) } time.Sleep(time.Second *5) //不等待会提前结束计算,未计算的线程将被退出 lock.Lock() for i,v := range myMap { fmt.Println("map[",i,"]=",v) } lock.Unlock() }
通过加互斥锁(同步锁)的方式,并发进行运算、添加,但是这种方式也有缺点:
- 前面使用全局变量加锁同步来解决
goroutine
的通讯,但不完美 - 主线程在等待所有
goroutine
全部完成的时间很难确定,我们这里设置 5 秒,仅仅是估算。 - 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有
goroutine
处于工作状态,这时也会随主线程的退出而销毁。 - 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
还可以用
channel来解决:
func add(s []int , c chan int) { sum := 0 for _, v := range s { sum += v fmt.Println(v) } c <- sum } func main() { c := make(chan int) s :=[]int{2,5,9,23,7,3,4} go add(s[:len(s)/2 ] ,c) //写channel操作会阻塞,直到读channel操作执行 go add( s[len(s)/2:] ,c) x ,y:= <-c ,<-c //随机并发 fmt.Println(x ,y ,x+y) }
信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。
ch <- v // 将 v 发送至信道 ch。 v := <-ch // 从 ch 接收值并赋予 v。
(“箭头”就是数据流的方向。)
和映射与切片一样,信道在使用前必须创建:
ch := make(chan int)
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得
Go程可以在没有显式的锁或竞态变量的情况下进行同步。
channel
是线程安全的;channel
本质是队列,遵循先进先出;channel
中只能存放指定的数据类型;channel
的数据放满后,就不能再放入;如果从
channel
取出数据后,可以继续放入;在没有使用协程的情况下,如果
channel
数据取完了,再取,就会报deadlock
。
还可以放进任意类型(
interface{})的数据:
package main import "fmt" type Student struct { Name string `json:"name"` // 是 ` ` (tab键上的~按键) ,不是 ' ' Sex string `json:"sex"` } func main() { allChan := make(chan interface{},5) stu1 := Student{Name: "lili",Sex: "f"} stu2 := Student{Name: "chang",Sex: "m"} stu3 := Student{Name: "ling",Sex: "m"} allChan <- stu1 allChan <- 10 allChan <- stu2 allChan <- 99.5 allChan <- stu3 <- allChan <- allChan stuRes := <- allChan fmt.Println(stuRes) //读取结构体类型数据字段,需要先进行类型断言 stu := stuRes.(Student) fmt.Println(stu.Name) fmt.Println(stu.Sex) }
由于
channel是
interface{}类型,所以使用的时候,都需要先进行类型断言。
allChan <- myMap <- allChan <- allChan stuMap := <- allChan stus := stuMap.(map[int]Student) fmt.Println(stus) fmt.Println(stus[0]) fmt.Println(stus[1])
allChan <- stu1 allChan <- 10 allChan <- stu2 allChan <- 99.5 allChan <- stu3 <- allChan n := <- allChan n += 1 //报错
不使用类型断言,直接使用将会报错。因为编译器并不认识此类型,需要经过类型断言进行确认。
channel的关闭
channel关闭使用
close(chan),关闭
channel。
关闭后不能再向
channel发送数据,只能从
channel读取数据。
在上面的例子中的
<-allChan前加入以下代码:
close(allChan)
channel的遍历
遍历
channel之前需要关闭
channel,否则会报错(
deadlock)。
关闭
channel后,即可正常进行遍历
channel,知道遍历完成,退出遍历。
close(allChan)for v := range allChan { fmt.Println(v) }
只读、只写 channel
- 只读
channel
:(例如)
var chan1 <-chan int
- 只写
channel
:(例如)
var chan2 chan<- int
案例:
func send(ch chan<- int,exit chan struct{}) { for i := 0; i < 10; i++ { ch <- i fmt.Println("输入",i) } close(ch) var a struct{} exit <- a } func get(ch <-chan int,exit chan struct{}) { for i := 0; i < 10; i++ { v,ok := <-ch if !ok { break } fmt.Println("输出",v) } var a struct{} exit <- a } func main() { ch := make(chan int ,10) //双向通道 exitChan := make(chan struct{} ,2) go send(ch,exitChan) go get(ch,exitChan) for { if len(exitChan) == 2 { break } } }
可以使用
for+
select语句防止阻塞:
func main() { ch := make(chan int ,10) for i := 0; i < 5; i++ { ch <- i } ch2 := make(chan float64 ,10) for i := 0; i < 5; i++ { ch2 <- rand.Float64() } label: for { select { case n:= <-ch: fmt.Println(n) time.Sleep(time.Second) case m:=<-ch2: fmt.Println(m) time.Sleep(time.Second) default: fmt.Println("没了") time.Sleep(time.Second) //return 直接结束退出程序运行 break label //中断指定的 for 循环 } } }
使用
defer+
recover解决运行时协程中抛出的
panic,保证程序继续运行:
func wrong() { defer func(){ if e := recover() ; e != nil { fmt.Print("func wrong()计算错误,") fmt.Println("异常",e) } }() num := 0 num1 := 100 num = num1 /num fmt.Println("func wrong()计算正确",num) } func right() { defer func(){ if e := recover() ; e != nil { fmt.Println(e) } }() num := 10 num1 := 100 num = num1 /num fmt.Println("func right() 计算正确",num) } func main() { go wrong() go right() for i := 0; i < 5; i++ { time.Sleep(time.Second) i++ } }
关于
recover:
内建函数recover允许程序管理恐慌过程中的Go程。在defer的函数中,执行recover调用会取回传至panic调用的错误值,恢复正常执行,停止恐慌过程。若recover在defer的函数之外被调用,它将不会停止恐慌过程序列。在此情况下,或当该Go程不在恐慌过程中时,或提供给panic的实参为nil时,recover就会返回nil。
相关文章推荐
- 第十七章:Go语言goroutine和channel
- golang 部分理解:关于channel 和 goroutine 例子
- 关于goroutine和channel的使用和个人见解
- go语言之行--golang核武器goroutine调度原理、channel详解
- 关于Golang中for-loop与goroutine的问题详解
- 使用channel实现goroutine
- 一个Golang例子:for + goroutine + channel
- Go语言学习——channel的死锁其实没那么复杂
- Golang goroutine and channel
- 关于.net 2.0 remoting 中 TCP Channel 用户认证探讨(一)
- golang语言并发与并行——goroutine和channel的详细理解
- 关于flume的filechannel的 full 问题
- Goroutine + Channel 实践
- Go-Goroutine-Channel-基础理解
- Golang进阶之关于channel全面分析(三)
- 关于.net 2.0 remoting 中 TCP Channel 用户认证探讨(二)
- 使用goroutine+channel和java多线程+queue队列的方式开发各有什么优缺点?
- 关于go语言
- 关于Go语言daemon启动的方法.
- go 使用 goroutine channel 通信