goroutine
goroutine是Go语言中的轻量级线程实现,由Go运行时管理,其使用方式也特别简单,只需要加上go关键字就可以了。
func StudyGoroutine() {
go newTask() // 使用go关键字,新建一个协程
for {
fmt.Println("this is test1 goroutine")
time.Sleep(time.Second) // 延时1s
}
}
func newTask() {
for {
fmt.Println("this is newTask goroutine")
time.Sleep(time.Second) // 延时1s
}
}
runtime.Gosched() —— 让出时间片
- runtime.Gosched()用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
- 类似于一场接力赛,A跑了一段时间碰到代码runtime.Gosched(),然后就把接力棒交给B了,A歇着了,B继续跑。
func StudyGosched() {
go func() {
for i := 0; i < 5; i++ {
fmt.Println("go")
}
}()
for i := 0; i < 2; i++ {
// 让出时间片,先让别的协程执行,它执行完,再回来执行此协程
runtime.Gosched()
fmt.Println("hello")
}
}
runtime.Goexit() —— 终止所在的协程
- runtime.Goexit()会立即终止当前的goroutine执行,调度器确保所有已经注册的defer延迟调用执行
func StudyGoexit() {
// 创建一个新的协程
go func() {
fmt.Println("aaaaaaaaa")
// 调用别的函数
test()
fmt.Println("bbbbbbbbbbbb")
}()
// 这里写一个死循环,目的不让主协程结束
for {
}
}
func test() {
defer fmt.Println("ccccccccccccc")
//return // 终止此函数,
//打印 aaaaaaaa ccccccccc bbbbbbbb
//下面打印的 dddddddddd 不再打印,因为执行不到下面
runtime.Goexit() // 终止所在的协程
// 当执行到这一句时,当前协程会被终止,dddddd和bbbbbb 都不会打印
// 最终打印 aaaaaaa cccccccc
fmt.Println("dddddddddddddd")
}
runtime.GOMAXPROCS() —— 设置CPU核数
- runtime.GOMAXPROCS()用来设置可以并行计算的CPU核数的最大值,并返回之前的值
func StudyGOMAXPROCS() {
n := runtime.GOMAXPROCS(1) // 指定以单核运算
fmt.Println(n)
for {
go fmt.Println(1)
fmt.Println(2)
// 当单核运算时,只能连续打印11111 或者连续打印22222,隔很长之后才会交替打印
// 当多核运算时,打印1和2之间的间隔会比单核小很多
}
}
channel
- channel是Go语言在语言级别提供的goroutine间的通信方式。我们可以使用channel在两个或者多个goroutine之间传递消息。
- channel是进程间的通信方式,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等
基本语法
- 一般channel的声明格式为
var chanName chan ElementType
。与一般的变量不同的地方仅仅在类型之前加了chan关键字。ElementType指定这个channel能传递的元素类型。 - 定义一个channel也很简单,直接使用内置make()使用即可。
ch := make(chan int)
这就声明并且初始化了一个int型的名为ch的channel - 在channel的用法中,最常见的就是写入和读出了。
- 将一个数据写入channel:
ch <- value
,向channel写入数据通常会造成阻塞,直到有其他goroutine从这个channel中读取数据; - 从channel中读取数据的语法是
value := <- ch
,如果channel之前没有写入数据,那么从channel中读取数据也会导致阻塞,直到channel中被写入数据为止。
- 将一个数据写入channel:
代码实践一下channel
func test1() {
// 新建两个协程,代表2个人,2个人同时使用打印机
go person1()
go person2()
// 为了不让主协程结束,来一个死循环
// 不进行channel相关操作的时候,
//打印结果是 “hwoelrllod” 也就是hello world 这两个协程随机获取到时间片,然后一个字符一个字符的打印
for {
}
}
func person2() {
<-ch // 从管道读取数据(接收),如果通道没有数据就会阻塞
Printer("world")
}
// person1执行完后,person2再执行
func person1() {
Printer("hello")
ch <- 666 // 给管道写数据(发送)
}
func Printer(s string) {
for _, data := range s {
fmt.Printf("%c", data)
time.Sleep(time.Second)
}
fmt.Printf("\n")
}
通过channel 实现同步和数据交互
func test2() {
// 创建channel
ch1 := make(chan string)
defer fmt.Println("主协程也结束")
go func() {
defer fmt.Println("子协程调用完毕")
for i := 0; i < 2; i++ {
fmt.Println("子协程 i = ", i)
time.Sleep(time.Second)
}
ch1<- "我是子协程,我工作完毕"
}()
str := <-ch1 // 没有数据前,会阻塞
fmt.Println("str = ", str)
}
缓冲机制
之前示范创建使用的channel都是无缓冲的channel,这种对于传递单个数据的场景可以接受,但是如果是需要持续传输大量数据的场景就不合适了。
func test3() {
// 创建一个无缓冲的channel
ch2 := make(chan int)
// len(ch2)表示缓冲区剩余数据的个数,cap(ch2)表示缓冲区大小
fmt.Printf("len(ch2) = %d, cap(ch2) = %d\n", len(ch2), cap(ch2))
// 新建协程
go func() {
for i := 0; i < 3; i++ {
fmt.Println("子协程i = ", i)
ch2 <- i // 往channel写内容
}
}()
// 延时
time.Sleep(2 * time.Second)
for i := 0; i < 3; i++ {
num := <- ch2 // 读channel中的内容,没有内容前会阻塞
fmt.Println("num = ", num)
}
}
下面,就看看怎么给channel带上缓冲,从而达到消息队列的效果:
c := make(chan int, 1024)
在调用make()函数的时候,将缓冲区大小作为第二个参数传入,比如上面这个例子就创建了一个大小为1024的int类型的channel。在带有缓冲的channel时,即使没有读取方,写入方也可以一直往channel中写入,在缓冲区被填满之前都不会阻塞
func test4() {
// 创建一个有缓冲的channel
ch2 := make(chan int, 3)
// len(ch2)表示缓冲区剩余数据的个数,cap(ch2)表示缓冲区大小
fmt.Printf("len(ch2) = %d, cap(ch2) = %d\n", len(ch2), cap(ch2))
// 新建协程
go func() {
for i := 0; i < 10; i++ {
ch2 <- i // 往channel写内容
fmt.Printf("子协程[%d]: len(ch2) = %d, cap(ch2) = %d\n", i, len(ch2), cap(ch2))
}
}()
// 延时
time.Sleep(2 * time.Second)
for i := 0; i < 10; i++ {
num := <- ch2 // 读channel中的内容,没有内容前会阻塞
fmt.Println("num = ", num)
}
}
channel的关闭
- channel不像文件一样,需要经常去关闭,只有当你确认没有任何数据发送了,或者你想显示的结束range循环之类的,才会去关闭channel
- 关闭channel后,无法向channel再发送数据(引发panic错误后导致接收立即返回零值)
- 关闭channel后,可以继续向channel接收数据
- 对于nil channel,无论收发都会被阻塞
func test5() {
ch2 := make(chan int)// 创建一个无缓冲的channel
// 新建一个goroutine
go func() {
for i := 0; i < 5; i++ {
ch2 <- i // 往通道里写数据
}
// 当不需要写数据时,关闭通道
close(ch2)
}()
for true {
if num, ok := <-ch2; ok == true {
// 如果ok == true,那就说明channel还没有关闭
fmt.Println("num = ", num)
} else {
fmt.Println("通道关闭")
break
}
}
// 相比较上面这样遍历channel,有一种更简单的遍历方法
for num1 := range ch2 {
fmt.Println("num1 = ", num1)
}
}
单向channel
- 默认情况下,channel是双向的,可以读数据,也可以进行写数据
- 单向channel只能进行写或者读:chan <- 表示进入管道,也就是写数据; <- chan 表示从管道出来,也就是读数据
func test6() {
var ch1 chan int // 普通双向channel
var ch2 <- chan int // 只能读数据的channel
var ch3 chan <- int // 只能写数据的channel
var ch4 chan <- int = ch1 // 双向channel可以隐式的转换为单向channel,单向无法转化为双向
<- ch2
ch3 <- 666
ch4 <- 777
}
单向channel的应用
func test7() {
// 创建一个双向channel
ch2 := make(chan int)
// 生产者,生产数字,写入channel
// 新创建一个协程
go producer(ch2) // channel传参,属于引用传递
// 消费者,从channel读取内容,打印
consumer(ch2)
}
// 这个函数只用读,不用写
func consumer(in <- chan int) {
for num := range in {
fmt.Println("num = ", num)
}
}
// 这个函数只用写,不用读,所以使用单向channel
func producer(out chan <- int) {
for i := 0; i < 10; i++ {
out <- i * i
}
close(out)
}
定时器Timer和Ticker
Timer
- 代表未来的一个单一事件,你可以告诉Timer你要等待多长时间,它提供一个channel,在将来的那个时间那个channel提供一个时间值。
- 也就是Timer在设置的时间到了之后,只会响应一次。
func LearnTimer() {
// 创建一个定时器,设置时间2s,2s后,它会往channel中写内容(当前时间)
timer := time.NewTimer(2*time.Second)
fmt.Println("当前时间:", time.Now())
// 2s过后,往timer.C写数据,有数据后,就可以读取了,没有数据前会阻塞
t := <- timer.C
fmt.Println("t = ", t)
}
通过Timer实现延时功能
func testTimer() {
// 延时2s后打印一句话
// 1. 通过Timer实现
timer := time.NewTimer(2 * time.Second)
<-timer.C
fmt.Println("时间到")
// 2,通过Sleep实现
time.Sleep(2 * time.Second)
fmt.Println("时间到")
// 3. 通过After实现
<-time.After(2 * time.Second) // 定时2s,阻塞2s,2s后产生一个事件,往channel中写内容
fmt.Println("时间到")
}
停止定时器
func StopTimer() {
timer := time.NewTimer(3 * time.Second)
go func() {
<-timer.C // 定时器停止之后,这句就无效了
fmt.Println("子协程可以打印了,因为定时器的时间到了")
}()
timer.Stop() //停止定时器
for true {
fmt.Println("主协程")
}
}
重置定时器
func ResetTimer() {
// 将3s重置为2s
timer := time.NewTimer(3 * time.Second)
ok := timer.Reset(2 * time.Second)
if ok == true {
<- timer.C
fmt.Println("2s时间到")
} else {
<-timer.C
fmt.Println("3s时间到")
}
}
Ticker
- Ticker是一个定时触发的计时器,它会以一个间隔往channel发送一个事件(当前时间),而channel的接收者可以以固定时间间隔从channel中读取事件。
- 其实就是像上闹钟一样,循环周期性发送事件。
func LearnTicker() {
ticker := time.NewTicker(2 * time.Second)
i := 0
for {
<- ticker.C // 设置没2s打印一次
i++
fmt.Println("i = ", i)
// 设置5s后停止
if i == 5 {
ticker.Stop()
break
}
}
}