学习来源:
Go语言实战、blibli 黑马程序员 20小时快速入门go语言(中)
Go语言的优势
- Go语言设计简单
- Go语言从语言层面就支持并发。 btw,并发程序的内存管理有时候是非常复杂的,而Go语言提供了自动垃圾回收机制。
Go语言为并发编程而内置的上层API基于CSP(communicating sequentical process, 顺序通信)模型。
一、goroutine
1.1 goroutine
goroutine是Go的协程,是比线程更小的单位。(可以理解成“一个协程对应一个任务”)
goroutine是Go并发设计的核心。goroutine比thread更易用、更高效、更轻便。
func main( ) {
go newTask( ) //新建一个协程,新建一个任务
//该新建的协程可以与 主协程/任务 同时执行
for {
fmt.Println("this is a main goroutine.")
time.Sleep(time.Second) //延时1s
}
}
新建协程的方法:go+函数
任务调度器自动调度,可使得两个任务同时并发进行。
主协程和子协程执行的先后顺序主要与调度性能有关(可能会随机)
当主协程退出后,其他子协程也会跟着退出。
- 主协程先退出导致子协程没有来得及调用的例子如下:
package main
import(
"fmt"
"time"
)
//主协程退出了,其他子协程也要跟着退出
func main( ) {
go fun( ) { //匿名函数
i := 0
for {
i++
fmt.Println("子协程 i = " , i)
time.Sleep(time.Second)
}
} ( ) //!!!别忘了 ( )
}
1.2 runtime包
1.2.1 Gosched
runtime.Gosched( ) 用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
package main
import (
"fmt"
"runtime"
)
func main( ) {
go func( ) {
for i:= 0; i < 5; i++ {
fmt.Println("go")
}
} ( )
for i:= 0; i < 2; i++ {
// 让出时间片,先让别的协议执行,它执行完,再回来执行此协程。
runtime.Gosched( )
fmt.Println("hello")
}
未加runtime.Gosched( ) 之前,执行结果只会输出hello,但是加了runtime.Gosched( ) 后,执行结果不仅有hello,还会有go。
1.2.2 Goexit
runtime.Goexit( ) 的作用是终止所在的协程
package main
import (
"fmt"
"runtime"
)
func test( ) {
defer fmt.Println("ccc")
runtime.Goexit( ) //终止所在的协程
fmt.Println("ddd")
}
func main( ) {
go func( ) { //创建新的协程
fmt.Println("aaa")
test( )
fmt.Println("bbb")
} ( ) //别忘了( )
//特地写了一个死循环,目的是不让主协程结束
for { }
}
//执行结果: aaa ccc
1.2.3 GOMAXPROCS
调用runtime.GOMAXPROCS( ) 用来设置可以并行计算的CPU核数的最大值,并返回之前的值。
多核的好处是使得协程之间的交替频率变大
n := runtime.GOMAXPROCS( )
//指定以单核运算,并返回指定运算前的CPU的核数,赋给n。
二、channel
channel 的存在是为了避免协程与协程交替进行而导致信息混乱的结果。
2.1 channel概述
通过channel实现同步
// 举个打印机的例子
package main
import (
"fmt"
"time"
)
// 全局变量,创建一个channel
var ch = make(chan int)
//定义一个打印机,参数为字符串,按每个字符打印
//打印机属于公共资源
func Println(str string) {
for _, data := range str {
fmt.Println("%c", data)
time.Sleep(time.Second)
}
fmt.Println("\n")
}
//person1执行完后才能到person2执行
func person1( ) {
Printer("hello")
ch <- 666 //给管道写入666(int型)数据
}
func person2( ) {
<-ch //从管道取数据。接受。 如果通道没有数据就会阻塞。
Printer(“world”)
}
func main( ) {
// 新建2个协程,代表2个人,2个人同时使用打印机
go person1( )
go person2( )
//特地不让主协程结束,死循环。
for { }
}
2.2 无缓冲的channel
ch<- 往管道 ch 写入数据
创建格式:
make (chan Type) //等价于 make (chan Type, 0)
协程1是把数据写入通道中去后,如果对方没有把数据取走之前,是会阻塞的。
eg: ch := make(chan int, 0)
// len(ch) = 0, cap(ch) = 0
2.3 有缓冲的channel
创建一个有缓冲的channel
ch := make(chan int, 3) // len(ch) = 0, cap(ch) =3
如果给定了一个缓冲区容量,通道就是异步的。
只要缓冲区有未使用空间用于发送数据,或还包含可以接受的数据,那么其通信就会无阻塞的进行。
2.4 range和close
关闭channel
package main
import("fmt")
func main( ) {
ch := make(chan int) //创建一个无缓冲的channel
//新建一个goroutine
go func( ) {
for i:= 0; i < 5; i++ {
ch <- i ; //往通道写数据
}
// 不需要再写数据时,关闭channel
close(ch) // 若注释掉 close(ch) ,程序会一直阻塞在此行。
} ( ) //别忘了 ( )
for {
// 如果OK为true,说明管道没有关闭。
//如果为false,说明管道已经关闭。
if num, OK := <-ch; OK == true {
fmt.Println("num = " , num)
} else { //管道关闭
break
}
}
}
btw,关闭channel后无法再发送数据,但是可以继续读数据。
2.5 单方向的channel
//创建一个channel,双向的
ch := make(chan, int)
//双向 channel 能隐式转换为单向 channel
var writeCh chan <- int = ch //只能写,不能读
writeCh <- 666 //写数据
var readCh <- chan int = ch //只能读,不能写
<- readCh
btw, 单向无法转换为双向
var ch2 chan int = writech // 会报错
单向channel的应用
package main
import ( "fmt")
//此通道只能写,不能读
func producer(out chan <- int) {
for i:= 0; i < 10; i++ {
out <- i*i
}
close(out) //关闭通道后只能读,不能写
}
//此 channel 只能读,不能写
func consumer(in <- chan int) {
for num := range in {
fmt.Println("num = ", num)
}
}
func main( ) {
//创建一个双向通道
ch := make(chan int)
//生产者 生产数字,写入channel
//新开一个协程
go producer (ch) //channel 传参,引用传递
//消费者,从channel读取内容后,打印
consumer(ch)
}