go-goroutine channel
goroutine 协程
基本介绍
- 进程是程序在操作系统的一次执行过程,是系统进行资源分配和调度的基本单位
- 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
- 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
- 一个程序至少有一个进程,一个进程至少有一个线程
- 并发和并行:
- 多线程程序在单核上运行,就是并发
- 多线程程序在多核上运行,就是并行
- 并发:因为是在一个cpu上,比如有10个线程,每个线程执行10ms,从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发
- 并行:因为是在多个cpu上,比如10核,10个线程,每个线程执行10ms,从人的角度看,这10个线程都在运行,但是在微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行
- go携程和go主线程
- go主线程(有程序员直接称为线程/也可以理解为进程):一个go线程上,可以起多个协程,协程是轻量级的线程【编译器做优化 】
- go协程的特点:
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
func test() {
for i := 0; i < 10; i++ {
fmt.Println("test hello world" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() { // 串行 -- 主线程
go test() // 开启协程 : 当主线程结束后,协程自动结束
for i := 0; i < 5; i++ {
fmt.Println("main hello golang")
time.Sleep(time.Second)
}
}
- 主线程是一个物理线程,直接作用在cpu上的,是重量级的,非常耗费cpu资源
- 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
- golang的协程机制是重要的特点,可轻松的开启上万个协程。其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这就凸显了golang在并发上的优势
goroutine的调度模型-MPG
- M:操作系统的主线程
- P:协程执行需要的上下文
- G:协程
-
为了充分利用多CPU的优势,在golang程序中,设置运行的cpu数目
cpuNum := runtime.NumCPU()
-
go1.8后,默认让程序运行在多个核上,可以不用设置
-
Go1.8前,还要设置一下,可以更高效的利用cpu
runtime.GPMAXPROCS(num)
channel 管道
需求
- 当存在资源竞争时,我们需要引入管道
import (
"fmt"
"time"
)
var MyMap map[int]int = make(map[int]int)
func CalN(n int) {
res := 1
for i := 2; i <= n; i++ {
res *= i
}
MyMap[n] = res
}
func main() {
for i := 1; i <= 200; i++ {
go CalN(i)
}
time.Sleep(time.Second * 3)
for index, val := range MyMap {
fmt.Printf("Map[%v] = %v\n", index, val)
}
}
加锁
var (
MyMap map[int]int = make(map[int]int)
// 声明一个全局的互斥锁
// lock 是一个全局的互斥锁
// sync synchronize 同步 mutex 互斥
lock sync.Mutex
)
func CalN(n int) {
res := 1
for i := 2; i <= n; i++ {
res *= i
}
// 加锁
lock.Lock()
MyMap[n] = res // 会产生资源竞争
lock.Unlock()
}
channel 管道
- 加锁的问题:
- 主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置为3s,仅仅是估算
- 若主线程休眠时间过长,会增加等待时间,若休眠时间短,则会使某些goroutine未完成操作就被销毁了
- 通过全局变量加锁同步来实现通讯,也并不利用多个goroutine对全局变量的读写操作
介绍
- channel本质就是一个数据结构-队列
- 数据是先进先出
- 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
- channel是有类型的,一个string的channel只能存放string类型
基本使用
- 定义和声明
var 变量名 chan 数据类型
var intChan chan int
var stringChan chan string
var sliceChan chan []float64
var mapChan chan map[int]string
var personChan chan Person
var personChan2 chan *Person
- channel是引用类型
- channel必须初始化才能写入数据,即make后才能使用
- 管道是有类型的,intChan只能写入整数 int
func main() {
// 1. 定义或声明 make空间
var intChan chan int = make(chan int, 3)
// 2. 引用类型
fmt.Printf("intChan: %v\n", intChan) // intChan: 0xc0000260c0
// 3. 向管道写入数据
// 若存入数据时,超出了cap,则会报deadlock
intChan <- 20
var num int = 211
intChan <- num
// 4. len cap
fmt.Printf("len(intChan): %v\n", len(intChan)) // len(intChan): 2
fmt.Printf("cap(intChan): %v\n", cap(intChan)) // cap(intChan): 3
// 5. 读取数据
// 在没有使用goroutine的情况下,若我们的channel数据已经被全部取出,再去就会报告deadlock死锁
var num2 int = <-intChan
fmt.Printf("num2: %v\n", num2) // num2: 20
fmt.Printf("len(intChan): %v\n", len(intChan)) // len(intChan): 1
num3 := <-intChan
fmt.Printf("num3: %v\n", num3) // num3: 211
}
注意事项
- channel中只能存放指定的数据类型
- channel的数据放满后,就不能再放入了
- 若从channel取出数据后,可以继续放入
- 在没有使用协程的情况下,若channel数据取完了,再取的话,就会报 deadlock
案例
func main() {
// 1.
var intChan chan int = make(chan int, 6)
intChan <- 10
intChan <- 20
intChan <- 30
num1 := <-intChan
num2 := <-intChan
num3 := <-intChan
// num1: 10 num2: 20 num3: 30
fmt.Printf("num1: %v\tnum2: %v\tnum3: %v\n", num1, num2, num3)
// 2.
var mapChan chan map[string]string = make(chan map[string]string, 5)
var mapTemp1 map[string]string = make(map[string]string)
mapTemp1["tom"] = "12"
mapTemp1["jack"] = "23"
var mapTemp2 map[string]string = make(map[string]string)
mapTemp2["jacy"] = "12"
mapTemp2["mary"] = "23"
mapChan <- mapTemp1
mapChan <- mapTemp2
var mapTemp3 map[string]string = make(map[string]string)
mapTemp3 = <-mapChan
fmt.Printf("mapTemp3: %v\n", mapTemp3) // mapTemp3: map[jack:23 tom:12]
// 3.
var dog1 Dog = Dog{"tom", 22}
var dog2 Dog = Dog{"jack", 33}
var dogChan chan Dog = make(chan Dog, 5)
dogChan <- dog1
dogChan <- dog2
var dog3 Dog = <-dogChan
fmt.Printf("dog3: %v\n", dog3) // dog3: {tom 22}
// 4.
var dog4 Dog = Dog{"tom", 22}
var dog5 Dog = Dog{"jack", 33}
var dogChan2 chan *Dog = make(chan *Dog, 5)
dogChan2 <- &dog4
dogChan2 <- &dog5
var dog6 = <-dogChan2
fmt.Printf("dog6: %v\n", dog6) // dog6: &{tom 22}
// 5.
var allChan chan interface{} = make(chan interface{}, 5)
allChan <- "hello"
allChan <- 99
allChan <- dog1
allChan <- &dog2
allChan <- mapTemp1
str := <-allChan
fmt.Printf("str: %v\n", str) // str: hello
// 取出map
<-allChan
dog7 := <-allChan
<-allChan
mapTemp4 := <-allChan
fmt.Printf("mapTemp4: %v\n", mapTemp4) // mapTemp4: map[jack:23 tom:12]
fmt.Printf("dog7: %v\n", dog7)
// 7.
// dog7.Name // 错误,因为类型是interface{}
// 借助类型断言,进行转换
d := dog7.(Dog)
fmt.Printf("d.Name: %v\n", d.Name) // d.Name: tom
}
type Dog struct {
Name string
Age int
}
channel遍历和关闭
channel关闭
- 使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据
channel遍历
-
channel支持for-range方式的遍历
-
在遍历时,若channel没有关闭,则会出现deadlock
-
在遍历时,若channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
案例1
func main() {
numChan := make(chan int, 2000)
resChan := make(chan int, 2000)
exitChan := make(chan int, 8)
go writeNum(numChan)
for i := 0; i < 8; i++ {
go storeResult(resChan, numChan, exitChan)
}
for len(exitChan) != 8 {
}
close(resChan)
i := 0
for v := range resChan {
i++
fmt.Printf("res[%v]: %v\n", i, v)
}
}
func writeNum(numChan chan int) {
for i := 1; i <= 2000; i++ {
numChan <- i
}
close(numChan)
}
func storeResult(resChan chan int, numChan chan int, exitChan chan int) {
// 读取并计算保存
for v := range numChan {
res := 0
for i := 1; i < v; i++ {
res += i
}
resChan <- res
}
exitChan <- 1
}
案例2
import (
"fmt"
"time"
)
func main() {
numChan := make(chan int, 1000)
resChan := make(chan int, 2000)
exitChan := make(chan bool, 4)
startTime := time.Now().Unix()
go writeData(numChan)
for i := 0; i < 4; i++ {
go primeStore(numChan, resChan, exitChan)
}
var endTime int64 = 0
go func() {
for len(exitChan) != 4 {
}
endTime = time.Now().Unix()
close(resChan)
close(exitChan)
}()
for v := range resChan {
fmt.Printf("v: %v\t", v)
}
fmt.Println("main exit")
fmt.Printf("startTime: %v\n", startTime)
fmt.Printf("endTime: %v\n", endTime)
fmt.Println("time: ", (endTime - startTime))
}
// 写入
func writeData(numChan chan int) {
for i := 1; i <= 80000; i++ {
numChan <- i
}
close(numChan)
}
// 判断是否是素数,并保存到resChan中
func primeStore(numChan chan int, resChan chan int, exitChan chan bool) {
for v := range numChan {
flag := true
// 判断是否是素数
for i := 2; i <= v/2; i++ {
if v%i == 0 {
flag = false
break
}
}
if flag {
resChan <- v
}
}
exitChan <- true
fmt.Println("prime exit")
}
注意事项和细节
- channel可以声明为只读,或者只写性质
func main() {
// 1. 在默认情况下,channel是双向的
var chan1 chan int = make(chan int, 2)
fmt.Printf("chan1: %v\n", chan1)
// 2. 只写
var chan2 chan<- int = make(chan int, 3)
// num := <-chan2 //cannot receive from send-only channel chan2
fmt.Printf("chan2: %v\n", chan2)
// 3. 只读
var chan3 <-chan int = make(chan int, 4)
// chan3 <- 3 // cannot send to receive-only channel chan3
fmt.Printf("chan3: %v\n", chan3)
}
- 使用Select可以解决从管道取数据的阻塞问题
func main() {
intChan := make(chan int, 10)
stringChan := make(chan string, 5)
for i := 0; i < 10; i++ {
intChan <- i + 1
}
for i := 0; i < 5; i++ {
stringChan <- "string" + strconv.Itoa(i+1)
}
// 当不关闭channel,用传统的方法在遍历channel时,会导致deadlock
// 可以使用select 方式解决问题
for {
select {
case v := <-intChan:
fmt.Printf("intChan v: %v\n", v)
time.Sleep(time.Second)
case v := <-stringChan:
fmt.Printf("stringChan v: %v\n", v)
default:
fmt.Println("exit")
return
}
}
}
- goroutine中使用recover,解决goroutine中出现的panic,导致程序崩溃问题
import (
"fmt"
"time"
)
func sayHello() {
for i := 0; i < 10; i++ {
fmt.Printf("Hello-%v\n", i+1)
time.Sleep(time.Second)
}
}
func test() {
defer func() {
err := recover()
if err != nil {
fmt.Printf("err: %v\n", err)
}
}()
// 存在错误,会影响整个goroutine和main的执行
var myMap map[int]string
myMap[1] = "tom"
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Printf("go-%v\n", i+1)
time.Sleep(time.Second)
}
}