并发编程 | 青训营笔记

27 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天

前言

大家好呀,这是我参加青训营伴学笔记创作活动的第 6 天,如存在问题,烦请各位斧正!

其中有一些关键图片超过了最大字符限制,不能上传了,我都使用特殊标记给它标记出来了,如有需要,请联系我。

goroutine

1)在Go语言里,想要编写一个并发程序是非常容易的事情,它不需要额外引用其他的第三方库,只需要使用"go"关键字就可以实现。

2)优势:goroutine是Go并发设计的核心,也叫协程,它比线程更加轻量,因此可以同时运行成千上万个并发任务。

不仅如此,Go语言内部已经实现了goroutine之间的内存共享,它比线程更加易用、高效和轻便。

3)与线程相比:

(1)协程处于用户态、线程处于内核态,不需要有内核态的切换。 协程相当于轻量级线程。

(2)协程的切换时间点是由调度器决定,而不是由系统内核决定的。

(3)对垃圾回收有好处:如果垃圾回收时使用的是线程,那么系统势必会暂停所有线程。

但如果使用协程,调度器知道什么时候内存位于一致状态,所以也就没有必要暂停所有运行的线程。

(4)线程一般是固定的栈容量2MB,用于保存局部变量,在函数切换时使用。

但是对于goroutine来说,大小固定的栈可能会导致资源浪费,所以Go采用了动态扩张收缩的策略,初始化为2KB,最大可扩张到1GB。

4)Go语言的并发基于CSP(通信顺序进程)模型,CSP模型是用于描述两个独立的并发实体通过共享的通信管道(channel)进行通信的并发模型。

<图片:超过最大字节限制>

5)使用:在调用的函数前面添加go关键字,就能使这个函数以协程的方式运行。(调度器会自动将其安排到合适的系统线程上去执行。)

channel

概述

1)channel是golang在goroutine之间的通讯方式

2)channel是引用类型,使用的时候必须通过make进行初始化,make的channel打印结果是地址。

3)背景:全局变量加锁的方式来解决goroutine通讯的方式不完美,

主线程在等待所有goroutine全部完成的时间很难确定,所以这里需要一个管道channel来完成这种通讯连接。

4)channel的本质就是一个先进先出的数据结果队列,

它本身是线程安全的,因为本身就是阻塞模式,所以多个goroutine访问时,不需要加锁。

5)channel接收参数结束后,必须关闭,避免channel不断增加带来的内存泄漏。

(关闭channel的时候不要在接收端关闭channel)

使用

1)创建channel:make(chan 元素类型, 容量) // 不指定容量或容量为0 则为非缓冲区的channel,这必须在main以外的goroutine里写入,否则会报错。

比如 ch := make(chan int, 3)

2)发送:ch <- 10 // 把10传递给ch

3)接收:x := <-ch // 从ch中接收值并赋值给变量x

4)关闭管道:close(ch) // 一定要关闭管道,建议创建后使用defer关闭

select

概述

1)Go的select语句是一种仅能用于channl发送和接收消息的专用语句,此语句运行期间是阻塞的;当select中没有case语句的时候,会阻塞当前groutine。

2)select是Golang在语言层面提供的I/O多路复用的机制,其专门用来检测多个channel是否准备完毕:可读或可写。

3)select语句中除default外,每个case操作一个channel,要么读要么写。

4)select语句中除default外,各case执行顺序是随机的。

5)select语句中如果没有default语句,则会阻塞等待任一case。

6)select语句中读操作要判断是否成功读取,因为关闭的channel也可以读取。

基本使用

1)声明两个channel:chan1 := make(chan int) 和 chan2 := make(chan int)

2)

go func() {    // 同样再有一个func来向chan2写入       
  chan1 <- 1       
  time.Sleep(5 * time.Second)   
}()

3)

select {    
// select语句两个case分别检测chan1和chan2是否可读,如果都不可读则执行default语句。   
  case <-chan1:       
    fmt.Println("chan1 ready.")   
  case <-chan2:       
    fmt.Println("chan2 ready.")   
  default:       
    fmt.Println("default")   
}

4)select中各个case执行顺序是随机的:

(1)如果某个case中的channel已经ready,则执行相应的语句并退出select流程。

(2)如果所有case中的channel都未ready,则执行default中的语句然后退出select流程。

(3)并且由于协程和select语句并不能保证执行顺序,所以也有可能select执行时协程还未向channel中写入数据,所以select直接执行default语句并退出。

(4)假如select语句中如果没有default语句,则会阻塞等待任一case。

runtime包

概述

Go语言中就是runtime包实现了协程的小型任务调度器。

以下主要介绍三个函数:Gosched()Goexit()GOMAXPROCS()

1)runtime.Gosched()函数:使当前Go协程放弃处理器,以让其他Go协程运行。它不会挂起当前Go协程,因此当前Go协程未来会恢复执行。

2)Goexit() 终止调用它的Go协程,但其他Go协程不会受影响。Goexit()会在终止该Go协程前执行所有defer的函数。

3)GOMAXPROCS(n int)函数设置可同时执行的最大CPU数,并返回先前的设置。若n < 1,它就不会更改当前设置。(Go语言程序默认会使用最大CPU数进行计算。)

拓展

Go语言的协程是抢占式调度的,当遇到长时间执行或者进行系统调用时,会主动把当前goroutine的CPU转让出去,让其他goroutine能被调度并执行。

一般出现如下几种情况,goroutine就会发生调度:

(1)syscall

(2)C函数调用(本质上和syscall一样)

(3)主动调用runtime.Gosched

(4)某个goroutine的调用时间超过100ms,并且这个goroutine调用了非内联的函数。