Go语言快速上手(二) | 青训营笔记

1,964 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。

协程与管道

协程(Goroutine)

  • go不直接支持创建系统线程,协程是Go程序内部唯一的并发实现方式。
  • 起协程的语句:go func(){......}()
  • 注意主协程(main)结束后,此程序也就退出了,即使还有一些其他协程在运行。
  • 从古至今,多线程的优化有好多种方式,比较常见的一种是reactor模型:线程池中存储大量线程,需要多创建一个线程来执行任务时就从线程池中选取一个线程来用。
  • 而Go语言采用的是另一个思路:有几个核就跑几个线程,只是某个线程上面有很多协程;协程的切换是不会像线程切换那样有操作系统层面上的开销的,例如线程切换需要切换虚拟地址空间、切换内核栈、切换硬件上下文、CPUcache需要失效,而切换协程完全没有这些开销。
  • 协程底层的实现原理:基于GMP模型:
  • G:goroutines 表示一个协程
  • M:machine 表示一个线程
  • P:processor 管理器,通过队列管理协程

  • 基于GMP模型,协程运行在线程上
  • 一个协程中的信息:运行栈+寄存器数值(PC,BP,SP)
  • 协程的切换,仅仅需要改变寄存器的数值,cpu便会从需要切换的协程指定位置继续运行
  • 协程与线程的比例关系:N:M
协程:线程含义优点缺点
1:1一个协程在一个线程上运行(其实就是传统的多线程)利用多核上下文切换比较慢(reactor模型,代价较大)
N:1多个协程在一个线程上运行上下文切换较快1.无法充分利用多核2.饥饿,如果一个协程不结束,其余协程阻塞
N:M多个协程在多个线程上运行充分利用多核,上下文切换快对实现要求更高
  • 协程调度器的设计策略(减少开销、兼顾公平):

    • 复用线程(避免频繁的创建、销毁线程,而是对线程的复用)

      • work stealing机制:当本线程无可运行的协程时,尝试从其他线程绑定的P偷取G,而不是销毁线程。
      • hand off机制:当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。
    • 利用并行:GOMAXPROCS设置P的数量

    • 抢占:限制协程执行时长,不会出现饿死现象

    • 全局协程队列:多个线程全满时可以塞入全局协程队列,它是链表实现的,可以塞很多协程,不用怕没地方放协程。

  • 常用的同步控制机制:WaitGroup

    • 开发过程中,经常遇到多task之间的同步问题。例如,多个子task并发完成一部分任务,主task等待他们最后结束。
var wg sync.WaitGroup ;

for i := 0;i<3;i++ {

    wg.Add(1)

    go func(i int){

        wg.Done()

    }(i)

}

wg.Wait()

管道(Channel)

  • 并发模型CSP,全称Communicationg Sequential Processes。它的核心观念是将两个并发执行的实体通过管道连接起来,所有的消息都通过管道传输。

  • 管道(通道),也是一种Go的数据同步技术。它可以被看作是在一个程序内部的一个先进先出(FIFO:first in first out) 数据队列。

  • 管道的操作有读、写和关闭。

    • 定义:ch := make(chan string)
    • 读:a = <- ch
    • 写: ch <- "hello"
    • 写一个已经关闭的channel会引发panic
  • 管道分类:无缓冲管道&缓冲管道

  • 无缓冲管道:长度为0的channel,为不带buffer的channel

    • ch := make(chan int)
    • 不会发生额外的拷贝
    • 读在写前
  • 有缓冲管道:长度大于0的channel,为带buffer的channel

    • ch := make(chan int,10)
    • 会发生额外的拷贝
    • 写在读前 ch<- 1
    • 缓冲区最大为65535
  • 管道元素的传递,是复制,非缓冲区管道复制了1次,缓冲区管道复制了2次

  • 例如面试常考的:请用管道实现交替打印AB:

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)
	ch3 := make(chan string)
	go printA(ch1, ch2)
	go printB(ch1, ch2, ch3)
	<-ch3
}

func printA(ch1, ch2 chan string) {
	for i := 0; i < 100; i++ {
		<-ch2
		fmt.Println(i, "A")
		ch1 <- "print A"
	}
}

func printB(ch1, ch2, ch3 chan string) {
	ch2 <- "begin"
	for i := 0; i < 100; i++ {
		<-ch1
		fmt.Println(i, "B")
		if i != 99 {
			ch2 <- "print B"
		} else {
			ch3 <- "end"
		}
	}
}