前置知识点:(如果不了解建议百度下再学并发编程)
- 并发、并行,同时进行叫并行,交替进行叫并发(时间片轮循)。
- 进程、线程、协程
- 进程的状态,挂起、休眠、运行等等
- 进程的关系,比如子进程、子线程、孤儿进程、僵尸进程,守护进程等等。
协程 goroutine
从创建的消耗来比较,进程>线程>协程。
有很多时候我们只是想多个座位,结果却不得不去盖一栋房子。而协程就是这个凳子,进程就是房子。而进程和线程的消耗差距比这个要大几千倍。
创建
go func(){
}
package main
import (
"fmt"
"time"
)
func newTask() {
index := 1
fmt.Println("协程创建了")
for {
time.Sleep(time.Second * 2) //延时2s
fmt.Println("协程 index",index)
index ++
}
}
func main() {
go newTask() //新建一个协程, 新建一个任务
index := 1
//死循环
for {
time.Sleep(time.Second) //延时1s
fmt.Println("主进程 index",index)
index ++
}
}
输出:
协程创建了
主进程 index 1
协程 index 1
主进程 index 2
主进程 index 3
协程 index 2
主进程 index 4
主进程 index 5
协程 index 3
主进程 index 6
主进程 index 7
协程 index 4
主进程 index 8
...
和创建子进程或子线程是一样的逻辑,互不干扰。
当主进程或主协程停止的时候,子协程也会停止。
相关函数
- runtime.Gosched() 让出时间片
go func() {
for i := 0; i < 5; i++ {
fmt.Println("go")
}
}()
for i := 0; i < 2; i++ {
//让出时间片,先让别的协议执行,它执行完,再回来执行此协程
runtime.Gosched()
fmt.Println("hello")
}
- runtime.Goexit() 终止当前 goroutine 执⾏,调度器确保所有已注册 defer延迟调用被执行。
package main
import (
"fmt"
"runtime"
)
func test() {
defer fmt.Println("协程结束")
//return //终止此函数
runtime.Goexit() //终止所在的协程
fmt.Println("这句话不会被打印")
}
func main() {
//创建新建的协程
go func() {
fmt.Println("协程开始前")
//再创建一个
test()
runtime.Gosched()
fmt.Println("协程创建了")
}() //别忘了()
//死循环
for {}
}
- runtime.GOMAXPROCS() 设置可以并行计算的CPU核数的最大值,并返回之前的值。
比如有两个协程,设置1,就会交替执行,如果设置2,就会一起执行。
n := runtime.GOMAXPROCS(2) //指定以1核运算
//n := runtime.GOMAXPROCS(4) //指定以4核运算
fmt.Println("n = ", n)
for i:=1;i<100000;i++{
go fmt.Print(1)
fmt.Print(0)
}
从打印看效果不是很明显,但是可以用耗时任务来看。
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
n := runtime.GOMAXPROCS(2)
fmt.Println("n = ", n)
go func(){
for { //抢光所有资源
}
}()
for i:=1;i<100000;i++{
fmt.Print(0)
time.Sleep(time.Second)
}
}
这个就很明显了,如果只用1核,最多打印一个0,然后sleep,资源就全部被协程抢了,再也打印不出来了。但是如果用2核,则可以打印出来。
有人可能会说,不是说交替执行么,但是这里面的协程是死循环,事情一直没有完成,而主进程的sleep会表示我还可以等,则CPU不会让出时间片。
如果都是死循环会怎么样?会交替执行,因为主进程说,我也有急事,CPU则会让他们各自执行一段时间。
channel
之前的进程、线程之间,会通过共享内存来通信,但是数据都是存放在各自的家里。但是协程则是通过通信来共享内存。
channel本意是管道,控制并发最好的方法是什么?排队啊!管道就是将所有人的任务按顺序执行。
创建:
make(chan Type, capacity)
capacity表示缓存区大小
关闭:
close(chan Type)
package main
import (
"fmt"
"time"
)
//定义一个打印机,参数为字符串,按每个字符打印
//打印机属于公共资源
func Printer(str string) {
for _, data := range str {
fmt.Printf("%c", data)
time.Sleep(time.Second)
}
fmt.Printf("\n")
}
func person1() {
Printer("hello")
}
func person2() {
Printer("world")
}
//全局变量,创建一个channel
var ch = make(chan int)
//person3执行完后,才能到person4执行
func person3() {
Printer("HELLO")
ch <- 666 //给管道写数据,发送
close(ch)
}
func person4() {
//从管道取数据,接收,如果通道没有数据他就会阻塞,所有前面的会先执行
if num, ok := <-ch; ok == true {
fmt.Println("num = ", num)
} else { //管道关闭
fmt.Println("管道关闭了")
}
Printer("WORLD")
}
func main() {
//新建2个协程,代表2个人,2个人同时使用打印机
//没有管道的情况,会抢 输出:hwoelrllod
//go person1()
//go person2()
//有管道的情况,顺序执行:
/*
HELLO
num = 666
WORLD
*/
go person3()
go person4()
//特地不让主协程结束,死循环
for {
}
}
管道的缓冲:
无缓冲,则必须要发送方和接收方同时在才可以继续,否则发送方发了一个后就会阻塞。
有缓存,在缓冲大小内,发送方可以一直发,直到缓冲区满,接收方什么时候来收都可以。
//创建一个无缓存的channel
ch := make(chan int, 0)
//创建一个有缓存的channel
//ch := make(chan int, 3)
//len(ch)缓冲区剩余数据个数, cap(ch)缓冲区大小
fmt.Printf("len(ch) = %d, cap(ch)= %d\n", len(ch), cap(ch))
//新建协程
go func() {
for i := 0; i < 3; i++ {
fmt.Printf("子协程:i = %d\n", i)
ch <- i //往chan写内容
}
}()
//延时,没有缓存则发送方也会等待
time.Sleep(2 * time.Second)
for i := 0; i < 3; i++ {
num := <-ch //读管道中内容,没有内容前,阻塞
fmt.Println("num = ", num)
}
管道的方向限定
//创建一个channel, 双向的
ch := make(chan int)
//双向channel能隐式转换为单向channel
var writeCh chan<- int = ch //只能写,不能读
var readCh <-chan int = ch //只能读,不能写
writeCh <- 666 //写
<-readCh //读
网友评论