go goroutine 和 channel

goroutine

进程和线程

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

程序,进程和线程的关系

go goroutine 和 channel_第1张图片

并发和并行

  1. 多线程程序在单核上运行,就是并发
  2. 多线程程序在多核上运行,就是并行

go goroutine 和 channel_第2张图片

并发:
因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发

并行:
因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行

go协程和go主线程

go主线程(可以称为线程/也可以理解成进程):一个go线程上,可以起多个协程,协程是轻量级的线程【编译器做优化】

go协程的特点

  1. 有独立的栈空间
  2. 共享程序堆空间
  3. 调度由用户控制
  4. 协程是轻量级的线程

go goroutine 和 channel_第3张图片

goroutine案例

  1. 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1秒输出 “hello, world”
  2. 在主线程中也每隔1秒输出"hello, golang",输出10次后退出程序
  3. 要求主线程和goroutine同时执行
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)
	}
}

go goroutine 和 channel_第4张图片

说明:

  1. 主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
  3. golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显golang在并发上的优势

goroutine的调度模型

MPG模式

go goroutine 和 channel_第5张图片

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

MPG模式运行的状态1

go goroutine 和 channel_第6张图片

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

MPG模式运行的状态2

go goroutine 和 channel_第7张图片

  1. 分成两个部分来看
  2. 原来的情况是M0主线程正在执行G0协程,另外有三个协程在队列等待
  3. 如果G0协程阻塞,比如读取文件或者数据库等
  4. 这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行,M0的主线程下的G0仍然执行文件io的读写
  5. 这样的MPG调度模式,可以既让G0执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行
  6. 等到G0不阻塞了,M0会被放到空闲的主线程继续执行(从已有的线程池中取),同时G0又会被唤醒

设置golang运行的cpu数

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

package main

import (
	"fmt"
	"runtime"
)

func main()  {
	//获取当前系统cpu的数量
	num := runtime.NumCPU()
	//设置num-1的cpu运行go程序
	runtime.GOMAXPROCS(num)
	fmt.Println("num=", num)
}
  1. go1.8后,默认让程序运行在多个核上,可以不用设置了
  2. go1.8前,还要设置下,可以更高效的利用cpu

channel(管道)

计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。使用goroutine完成

package main

import (
	"fmt"
	"time"
)

//1.编写一个函数,计算各个数的阶乘,并放入到map中
//2. 启动的多个协程,统计的将结果放入到map中
//3. map应该做出一个全局的
var (
	myMap = make(map[int]int, 10)
)
//test函数就是计算n!,将这个结果放入到myMap
func test(n int)  {
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}
	myMap[n] = res
}
func main()  {
	//开启多个协程完成这个任务[200个]
	for i := 1; i <= 200; i++ {
		go test(i)
	}
	//休眠10秒
	time.Sleep(time.Second * 10)
	for i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}
}
# 启用数据数据竞争检测
go build -race main.go

go goroutine 和 channel_第8张图片

不同goroutine之间如何通讯

  • 全局变量的互斥锁
  • 使用管道channel

使用全局变量加锁同步改进程序

  • 没有对全局变量加锁,会出现资源争夺问题,代码会出现错误,提示 concurrent map writes
  • 解决:加入互斥锁
  • 数的阶乘很大,结果会越界,可以将求阶乘改成 sum += i
package main

import (
	"fmt"
	"sync"
	"time"
)

//1.编写一个函数,计算各个数的阶乘,并放入到map中
//2. 启动的多个协程,统计的将结果放入到map中
//3. map应该做出一个全局的
var (
	myMap = make(map[int]int, 10)
	//声明一个全局的互斥锁
	//lock 是一个全局的互斥锁
	//sync 是包: synchornized 同步
	//Mutex : 是互斥
	lock sync.Mutex
)
//test函数就是计算n!,将这个结果放入到myMap
func test(n int)  {
	res := 1
	for i := 1; i <= n; i++ {
		res += i
	}
	//res放入到myMap
	//加锁
	lock.Lock()
	myMap[n] = res
	//解锁
	lock.Unlock()
}
func main()  {
	//开启多个协程完成这个任务[200个]
	for i := 1; i <= 200; i++ {
		go test(i)
	}
	//休眠10秒
	time.Sleep(time.Second * 10)
	lock.Lock()
	for i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}
	lock.Unlock()
}

为什么需要channel

  1. 使用全局变量加锁同步解决goroutine的通讯, 不完美
  2. 主线程在等待所有goroutine全部完成的时间很难确定
  3. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁
  4. 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作

channel基本介绍

  1. channel本质就是一个数据结构-队列
  2. 数据是先进先出【FIFO: first in first out】
  3. 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
  4. channel有类型的,一个string的channel只能存放string类型数据

go goroutine 和 channel_第9张图片

定义/声明channel

var 变量名 chan 数据类型
var intChan chan int
var mapChan chan map[int]string
var perChan chan Person
var perChan2 chan *Person

说明:
channel是引用类型
channel必须初始化才能写入数据,即make后才能使用
管道是有类型的,intChan 只能写入整数int

管道的初始化,写入数据到管道,从管道读取数据注意事项

package main

import "fmt"

func main()  {
	//1.创建一个可以存放3个int类型的管道
	var intChan chan int
	intChan = make(chan int, 3)
	fmt.Printf("intChan 的值=%v intChan 本身的地址=%p\n", intChan, &intChan)

	intChan <- 10
	num := 211
	intChan <- num
	intChan <- 50
	<- intChan //从管道取出数据可以不接收,就扔掉了
	//intChan <- 98 //给管道写入数据时,不能超过其容量
	//管道的长度和cap(容量)
	fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))

	var num2 int
	num2 = <- intChan
	fmt.Println("num2=", num2)
	fmt.Printf("channel len=%v cap=%v \n", len(intChan), cap(intChan))

	//在没有使用协程的情况下,如果管道数据已经全部取出,再取就会报 deadlock
	num3 := <- intChan
	num4 := <- intChan
	//num5 := <- intChan
	fmt.Println("num3=", num3, "num4=", num4)
}

channel使用注意事项

  1. channel中只能存放指定的数据类型
  2. channel的数据放满后,就不能再放了
  3. 如果从channel取出数据后,可以继续放入
  4. 在没有使用协程的情况下,如果channel数据取完了,再取,就会报 deadlock

读写channel案例

  • 创建一个intChan,最多可以存放3个int,存放3个数据到intChan,然后再取出这三个int
package main

import "fmt"

func main()  {
	var intChan chan int
	intChan = make(chan int, 3)
	intChan <- 10
	intChan <- 20
	intChan <- 10
	num1 := <- intChan
	num2 := <- intChan
	num3 := <- intChan
	fmt.Printf("num1=%v num2=%v num3=%v \n", num1, num2, num3)
}
  • 创建一个mapChan,最多可以存放10个map[string]string 的 key-val
package main

import "fmt"

func main()  {
	var mapChan chan map[string]string
	mapChan = make(chan map[string]string, 10)
	m1 := make(map[string]string, 20)
	m1["city1"] = "北京"
	m1["city2"] = "天津"
	m2 := make(map[string]string, 20)
	m2["hero1"] = "宋江"
	m2["hero2"] = "武松"
	mapChan <- m1
	mapChan <- m2
	m3 := make(map[string]string, 20)
	m4 := make(map[string]string, 20)
	m3 = <- mapChan
	m4 = <- mapChan
	fmt.Printf("mapChan=%v\nm1=%v\nm2=%v\nm3=%v\nm4=%v\n", mapChan, m1, m2, m3, m4)
}
  • 创建一个catChan,最多可以存放10个Cat结构体变量
package main

import "fmt"

type Cat struct {
	Name string
	Age int
}
func main()  {
	var catChan chan Cat
	catChan = make(chan Cat, 10)
	cat1 := Cat{
		Name: "tom",
		Age:  10,
	}
	cat2 := Cat{
		Name: "jack",
		Age:  80,
	}
	catChan <- cat1
	catChan <- cat2
	cat11 := <- catChan
	cat22 := <- catChan
	fmt.Println(cat11, cat22)
}
  • 创建一个catChan2,最多可以存放10个*Cat变量
package main

import "fmt"

type Cat struct {
	Name string
	Age int
}
func main()  {
	var catChan chan *Cat
	catChan = make(chan *Cat, 10)
	cat1 := Cat{
		Name: "tom",
		Age:  10,
	}
	cat2 := Cat{
		Name: "jack",
		Age:  180,
	}
	catChan <- &cat1
	catChan <- &cat2
	cat11 := <- catChan
	cat22 := <- catChan
	fmt.Println(cat11, cat22)
}
  • 创建一个allChan,最多可以存放10个任意数据类型变量
package main

import "fmt"

type Cat struct {
	Name string
	Age int
}
func main()  {
	var allChan chan interface{}
	allChan = make(chan interface{}, 10)
	cat1 := Cat{
		Name: "tom",
		Age:  18,
	}
	cat2 := Cat{
		Name: "jack",
		Age:  180,
	}
	allChan <- cat1
	allChan <- cat2
	allChan <- 10
	allChan <- "mary"
	cat11 := <- allChan
	cat22 := <- allChan
	v1 := <- allChan
	v2 := <- allChan
	fmt.Println(cat11, cat22, v1, v2)
}
  • 管道中取出的结构体
package main

import "fmt"

type Cat struct {
	Name string
	Age int
}
func main()  {
	var allChan chan interface{}
	allChan = make(chan interface{}, 10)
	cat1 := Cat{
		Name: "tom",
		Age:  18,
	}
	cat2 := Cat{
		Name: "jack",
		Age:  180,
	}
	allChan <- cat1
	allChan <- cat2
	allChan <- 10
	allChan <- "mary"
	cat11 := <- allChan
	fmt.Printf("cat11=%T, cat11=%v\n", cat11, cat11)
	//使用类型断言
	a := cat11.(Cat)
	fmt.Printf("cat11.Name=%v\n", a.Name)
}

channel的遍历和关闭

channel的关闭

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

package main

import "fmt"

func main()  {
	intChan := make(chan int, 3)
	intChan <- 100
	intChan <- 200
	close(intChan) //关闭channel,不能再写了
	fmt.Println("ok")
	n1 := <- intChan
	fmt.Println("n1=", n1)
}
func close(c chan<- Type)

内建函数close关闭信道,该通道必须为双向的或只发送的。它应当只由发送者执行,而不应由接收者执行,其效果是在最后发送的值被接收后停止该通道。在最后的值从已关闭的信道中被接收后,任何对其的接收操作都会无阻塞的成功。对于已关闭的信道,语句:

x, ok := <-c

还会将ok置为false

channel的遍历

channel支持 for-range 的方式进行遍历

  1. 在遍历时,如果channel没有关闭,则会出现deadlock的错误
  2. 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
package main

import "fmt"

func main()  {
	intChan2 := make(chan int, 100)
	for i := 0; i < 100; i++ {
		intChan2 <- i * 2
	}
	close(intChan2)
	for v := range intChan2 {
		fmt.Println("v=", v)
	}
}

应用实例

goroutine和channel协同工作

  1. 开启一个writeData协程,向管道intChan中写入50个整数
  2. 开启一个readData协程,从管道intChan中读取writeData写入的数据
  3. 注意:writeData和readData操作的是同一个管道
  4. 主线程需要等待writeData和readData协程都完成工作才能退出

go goroutine 和 channel_第10张图片

package main

import "fmt"

//write Data
func writeData(intChan chan int)  {
	for i := 1; i <= 50; i++ {
		//放入数据
		intChan <- i
		fmt.Println("writeData ", i)
	}
	close(intChan)
}
//read Data
func readData(intChan chan int, exitChan chan bool)  {
	for  {
		v, ok := <- intChan
		if !ok {
			break
		}
		fmt.Printf("readData 读到数据=%v\n", v)
	}
	exitChan <- true
	close(exitChan)
}
func main()  {
	intChan := make(chan int, 50)
	exitChan := make(chan bool, 1)
	go writeData(intChan)
	go readData(intChan, exitChan)
	for  {
		_, ok := <- exitChan
		if !ok {
			break
		}
	}
}

应用实例-阻塞

func main()  {
	intChan := make(chan int, 10)
	exitChan := make(chan bool, 1)
	go writeData(intChan)
	//go readData(intChan, exitChan)
	for _ = range exitChan {
		fmt.Println("ok...")
	}
}

如果只是向管道写入数据 ,而没有读取,就会出现阻塞而deadlock,写管道和读管道的频率不一致,无所谓

应用实例3

统计1-200000的数字中,哪些是素数?
思路:

go goroutine 和 channel_第11张图片

package main

import (
	"fmt"
	"time"
)

//向intChan 放入1-8000个数
func putNum(intChan chan int)  {
	for i := 1; i <= 8000; i++ {
		intChan <- i
	}
	close(intChan)
}
//从intChan 取出数据,并判断是否为素数,如果是,就
//放入到primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool)  {
	var flag bool
	for  {
		time.Sleep(time.Millisecond * 10)
		num, ok := <- intChan
		if !ok {//intChan 取不到
			break
		}
		flag = true //假设是素数
		//判断num是不是素数
		for i := 2; i < num; i++ {
			if num % i == 0 {
				flag = false
				break
			}
		}
		if flag {
			//是素数,将这个数放入到primeChan
			primeChan <- num
		}
	}
	fmt.Println("有一个primeNum协程因为取不到数据,退出")
	//这里还不能关闭primeChan
	exitChan <- true
}
func main()  {
	intChan := make(chan int, 1000)
	primeChan := make(chan int, 2000)
	//标识退出的管道
	exitChan := make(chan bool, 4)
	//开启一个协程,向intChan放入1-8000个数
	go putNum(intChan)
	//开启4个线程,从intChan 取出数据,并判断是否为素数,如果是,就放入primeChan
	for i := 0; i < 4; i++ {
		go primeNum(intChan, primeChan, exitChan)
	}
	//主线程
	go func() {
		for i := 0; i < 4; i++ {
			<- exitChan
		}
		//当从exitChan取出了4个结果,就可以关闭primeChan
		close(primeChan)
	}()
	//变量primeChan,把结果取出
	for  {
		res, ok := <- primeChan
		if !ok {
			break
		}
		fmt.Printf("素数=%d\n", res)
	}
	fmt.Println("main 线程退出")
}

channel使用细节

  1. channel可以声明为只读,或者只写性质
package main

import "fmt"

func main()  {
	//默认情况下,管道是双向的(可读写)
	//管道声明为只写
	var chan2 chan<- int
	chan2 = make(chan int, 3)
	chan2 <- 20
	//num := <- chan2 //error
	fmt.Println("chan2=", chan2)
	//声明为只读
	var chan3 <-chan int
	num2 := <- chan3
	//chan3 <- 30 //error
	fmt.Println("num2", num2)
}
  1. channel只读和只写实战
package main

import "fmt"

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 := <- ch
		if !ok {
			break
		}
		fmt.Println(v)
	}
	var a struct{}
	exitChan <- a
}
func main()  {
	var ch chan int
	ch = make(chan int, 10)
	exitChan := make(chan struct{}, 2)
	go send(ch, exitChan)
	go recv(ch, exitChan)
	var total = 0
	for _ = range exitChan {
		total++
		if total == 2 {
			break
		}
	}
	fmt.Println("结束...")
}
  1. 使用select可以解决从管道取数据的阻塞问题
package main

import (
	"fmt"
	"time"
)

func main()  {
	//定义一个管道10个数据 int
	intChan := make(chan int, 10)
	for i := 0; i < 10; i++ {
		intChan <- i
	}
	//定义一个管道 5个数据 string
	stringChan := make(chan string, 5)
	for i := 0; i < 5; i++ {
		stringChan <- "hello" + fmt.Sprintf("%d", i)
	}
	//在实际开发中,可能不好确定什么时候关闭该管道,可以使用select方式解决
	for  {
		select {
		// 如果intChan一直没有关闭,不会一直阻塞,而会自动到下一个case匹配
		case v := <- intChan:
			fmt.Printf("从intChan读取的数据%d\n", v)
			time.Sleep(time.Second)
		case v := <- stringChan:
			fmt.Printf("从stringChan读取的数据%s\n", v)
			time.Sleep(time.Second)
		default:
			fmt.Printf("都取不到了")
			time.Sleep(time.Second)
			return
		}
	}
}
  1. goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题
    说明:
    如果起了一个协程,但是这个协程出现了panic, 如果没有捕获这个panic,就会造成整个程序崩溃,这时可以在goroutine中使用recover来捕获panic,进行处理,这样即使这个协程发生的问题,但是主线程仍然不受影响,可以继续执行
package main

import (
	"fmt"
	"time"
)

func sayHello()  {
	for i := 0; i < 10; i++ {
		time.Sleep(time.Second)
		fmt.Println("hello, world")
	}
}
func test()  {
	//使用defer + recover
	defer func() {
		//捕获test抛出的panic
		if err := recover(); err != nil {
			fmt.Println("test() 发生错误", err)
		}
	}()
	var myMap map[int]string
	myMap[0] = "golang" //error
}
func main()  {
	go sayHello()
	go test()
	for i := 0; i < 10; i++ {
		fmt.Println("main() ok=", i)
		time.Sleep(time.Second)
	}
}

你可能感兴趣的:(go)