一、goroutine
1、并发和并行:
多线程程序在单核上运行就是并发。
多线程程序在多核上运行就是并行。
2、Go协程和Go主线程
Go主线程(有人直接称为线程/也可以理解成进程):一个Go线程上,可以起多个协程,协程是轻量级的线程[编译器做优化]。
Go协程的特点:有独立的栈空间;共享程序堆空间;调度由用户控制;协程是轻量级的线程。
请编写一个程序,完成如下功能:
在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1秒输出 "hello,world"
在主线程中也每隔一秒输出"hello,golang", 输出10次后,退出程序
要求主线程和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)
}
}
主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源。
协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了。
3、goroutine的调度模型MPG
M指的是Machine,一个M直接关联了一个内核线程。由操作系统管理。 P指的是”processor”,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。它负责衔接M和G的调度上下文,将等待执行的G与M对接。 G指的是Goroutine,其实本质上也是一种轻量级的线程。包括了调用栈,重要的调度信息,例如channel等。
P的数量由环境变量中的GOMAXPROCS决定,通常来说它是和核心数对应,例如在4Core的服务器上回启动4个线程。G会有很多个,每个P会将Goroutine从一个就绪的队列中做Pop操作,为了减小锁的竞争,通常情况下每个P会负责一个队列。
三者关系如下图所示:
以上这个图讲的是两个线程(内核线程)的情况。一个M会对应一个内核线程,一个M也会连接一个上下文P,一个上下文P相当于一个“处理器”,一个上下文连接一个或者多个Goroutine。为了运行goroutine,线程必须保存上下文。
上下文P(Processor)的数量在启动时设置为GOMAXPROCS环境变量的值或通过运行时函数GOMAXPROCS()。通常情况下,在程序执行期间不会更改。上下文数量固定意味着只有固定数量的线程在任何时候运行Go代码。可以使用它来调整Go进程到个人计算机的调用,例如4核PC在4个线程上运行Go代码。
图中P正在执行的Goroutine为蓝色的;处于待执行状态的Goroutine为灰色的,灰色的Goroutine形成了一个队列runqueues。
Go语言里,启动一个goroutine很容易:go function就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,一旦上下文运行goroutine直到调度点,它会从其runqueue中弹出goroutine,设置堆栈和指令指针并开始运行goroutine。
能否抛弃P(Processor),让Goroutine的runqueues挂到M上呢?答案是不行,需要上下文的目的是:当遇到内核线程阻塞的时候可以直接放开其他线程。
一个很简单的例子就是系统调用sysall,一个线程肯定不能同时执行代码和系统调用被阻塞,这个时候,此线程M需要放弃当前的上下文环境P,以便可以让其他的Goroutine被调度执行。
如上图左图所示,M0中的G0执行了syscall,然后就创建了一个M1(也有可能来自线程缓存),(转向右图)然后M0丢弃了P,等待syscall的返回值,M1接受了P,将继续执行Goroutine队列中的其他Goroutine。
当系统调用syscall结束后,M0会“偷”一个上下文,如果不成功,M0就把它的Gouroutine G0放到一个全局的runqueue中,将自己置于线程缓存中并进入休眠状态。全局runqueue是各个P在运行完自己的本地的Goroutine runqueue后用来拉取新goroutine的地方。P也会周期性的检查这个全局runqueue上的goroutine,否则,全局runqueue上的goroutines可能得不到执行而饿死。
均衡的分配工作:按照以上的说法,上下文P会定期的检查全局的goroutine队列中的goroutine,以便自己在消费掉自身Goroutine队列的时候有事可做。假如全局goroutine队列中的goroutine也没了呢?就从其他运行的中的P的runqueue里偷。
每个P中的Goroutine不同导致他们运行的效率和时间也不同,在一个有很多P和M的环境中,不能让一个P跑完自身的Goroutine就没事可做了,因为或许其他的P有很长的goroutine队列要跑,得需要均衡。 该如何解决呢?Go的做法倒也直接,从其他P中偷一半!
4、设置golang运行的cpu数
为了充分利用多cpu的优势,在golang程序中可以设置运行cpu数目。 go1.8后,默认让程序运行在多个核上,可以不用设置。go1.8之前,需要设置一下,可以更高效的利用cpu。
package main
import (
"fmt"
"runtime"
)
func main() {
//获取当前系统cpu的数量
num := runtime.NumCPU()
//设置运行go程序的cpu数量
runtime.GOMAXPROCS(num)
fmt.Println("cpu number = ", num)
}
二、channel
计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。要求使用goroutine完成。
package main
import (
"fmt"
"time"
)
var (
myMap = make(map[int]int, 10)
)
func fac(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//将阶乘的计算结果放到map中
myMap[n] = res
}
func main() {
for i := 1; i <= 200; i++ {
go fac(i)
}
time.Sleep(time.Second * 10)
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
上述代码因为没有对全局变量myMap加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map writes
不同goroutine之间如何通讯:(1)、全局变量加入互斥锁;(2)、使用管道channel来解决。
为了解决上述代码中存在的资源竞争问题,全局变量myMap加入互斥锁。
package main
import (
"fmt"
"sync"
"time"
)
var (
myMap = make(map[int]int, 10)
//声明一个全局的互斥锁,
lock sync.Mutex
)
func fac(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//将阶乘的计算结果放到map中
//加锁
lock.Lock()
myMap[n] = res
//释放锁
lock.Unlock()
}
func main() {
for i := 1; i <= 20; i++ {
go fac(i)
}
time.Sleep(time.Second * 10)
lock.Lock()
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
lock.Unlock()
}
1、channel的基本介绍
channle本质就是一个数据结构-队列。
数据是先进先出【FIFO : first in first out】。
线程安全,多 goroutine 访问时,不需要加锁,就是说channel本身就是线程安全的。
channel有类型的,一个string的channel只能存放string类型数据。
2、声明channel
var 变量名 chan 数据类型
var intChan chan int (intChan 用于存放 int 数据)
var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型)
var perChan chan Person
var perChanPtr chan *Personchannel是引用类型
channel必须初始化才能写入数据, 即make后才能使用
管道是有类型的,intChan只能写入整数int
3、管道的初始化及读写数据
package main
import "fmt"
func main() {
//创建一个可以存放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 <- 80
//查看管道的长度和容量
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))
//从管道中读取数据
var n int
n = <-intChan
fmt.Println("n=", n)
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan))
//在没有使用协程的情况下,如果管道的数据已经全部取出,再取就会报告deadlock
num1 := <-intChan
num2 := <-intChan
//num3 := <-intChan
fmt.Println("num1=", num1, "num2=", num2)
}
channel中只能存放指定的数据类型
channle的数据放满后,就不能再放入了
如果从channel取出数据后,可以继续放入
在没有使用协程的情况下,如果channel数据取完了,再取就会报dead lock
4、练习题
(1)、创建一个intChan,最多可以存放3个int,存3个数据到intChan中,然后再取出这三个int。
package main
import "fmt"
func main() {
var intChan chan int
intChan = make(chan int, 10)
//intChan容量是3,再存放会报告deadlock
intChan <- 10
intChan <- 20
intChan <- 30
num1 := <-intChan
num2 := <-intChan
num3 := <-intChan
//intChany已经没有数据了,再取数据会报告deadlock
fmt.Printf("num1=%v num2=%v num3=%v", num1, num2, num3)
}
(2)、创建一个mapChan,最多可以存放10个map[string]string的key-value,对这个chan进行写入和读取。
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
mo1 := <-mapChan
mo2 := <-mapChan
fmt.Printf("mo1=%v\nmo2=%v", mo1, mo2)
}
(3)、创建一个catChan,最多可以存放10个Cat结构体变量,对这个chan进行写入和读取。
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: "nancy", Age: 78,}
catChan <- cat1
catChan <- cat2
c1 := <-catChan
c2 := <-catChan
fmt.Printf("c1=%v\nc2=%v", c1, c2)
}
(4)、创建一个catChanPtr,最多可以存放10个*Cat变量,对这个chan进行写入和读取。
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: "nancy", Age: 78,}
catChan <- &cat1
catChan <- &cat2
c1 := <-catChan
c2 := <-catChan
fmt.Printf("c1=%p\nc2=%p", c1, c2)
}
(5)、创建一个allChan,最多可以存放10个任意数据类型变量,对这个chan写入和读取。
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: 10,}
cat2 := Cat{Name: "nancy", Age: 78,}
allChan <- &cat1
allChan <- &cat2
allChan <- 10
allChan <- "jack"
c1 := <-allChan
c2 := <-allChan
v1 := <-allChan
v2 := <-allChan
fmt.Println(v1, v2, c1, c2)
}
5、channel的遍历和关闭
使用内置函数close可以关闭channel, 当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据。
package main
import "fmt"
func main() {
intChan := make(chan int, 3)
intChan <- 100
intChan <- 200
//关闭后不能再写数据
close(intChan)
//管道关闭之后,读取数据时可以的
n1 := <-intChan
fmt.Println("n1=", n1)
}
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
}
close(intChan)
for v := range intChan {
fmt.Println("v=", v)
}
}
使用goroutine和channel协调完成如下需求:
开启一个writeData协程,向管道intChan中写入50个整数;开启一个readData协程,从管道intChan中读取writeData写入的数据。writeData和readData操作的是同一个管道,主线程需要等待writeData和readData协程都完成才能退出。
package main
import "fmt"
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
intChan <- i
fmt.Println("writeData ", i)
}
close(intChan)
}
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
}
}
}
统计1-8000的数字中,哪些是素数?
package main
import (
"fmt"
"time"
)
func putNum(intChan chan int) {
for i := 1; i <= 8000; i++ {
intChan <- i
}
close(intChan)
}
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 {
break
}
flag = true
for i := 2; i < num; i++ {
if num%i == 0 {
flag = false
break
}
}
if flag {
primeChan <- num
}
}
fmt.Println("有一个primeNum协程因为取不到数据退出")
exitChan <- true
}
func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000)
exitChan := make(chan bool, 4)
go putNum(intChan)
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
go func() {
for i := 0; i < 4; i++ {
<-exitChan
}
close(primeChan)
}()
for {
res, ok := <-primeChan
if !ok {
break
}
fmt.Printf("素数=%d\n", res)
}
fmt.Println("main线程退出")
}
6、channel使用细节
(1)、默认情况下,管道是双向的,可读写的。channel可以声明为只读,或者只写性质。
package main
import "fmt"
func main() {
//声明为只写
var intChan chan<- int
intChan = make(chan int, 3)
intChan <- 20
fmt.Println("intChan=", intChan)
//声明为只读
var stringChan <-chan string
str := <-stringChan
fmt.Println("str=", str)
}
(2)、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("结束")
}
(3)、使用select解决从管道中取数据的阻塞问题
package main
import (
"fmt"
"time"
)
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 <- "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 := <-stringChan:
fmt.Printf("从stringChan读取的数据%s\n", v)
time.Sleep(time.Second)
default:
fmt.Printf("都取不到数据")
time.Sleep(time.Second)
return
}
}
}
(4)、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 func() {
if err := recover(); err != nil {
fmt.Println("test() 发生错误", err)
}
}()
var myMap map[int]string
//error,没有为map申请内存
myMap[0] = "golang"
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}