Golang-goroutine & channel

Golang-goroutine & channel

    • sync.Once的作用和用法?
    • 现有上亿行文件,想统计其中hello这个单词有多少个,如果用go实现,你有那些思路?
    • 用两个goroutine分别打印奇数和偶数,输出1~1000
      • 解题思路
      • 参考文档
    • 交替打印数字和字母: 使用两个 goroutine 交替打印序列,一个 goroutinue 打印数字, 另外一个goroutine打印字母, 最终效果如下 12AB34CD56EF78GH910IJ 。
      • 解题思路
      • 参考文档
    • 习题
      • 考点
      • 参看文档
    • 如何实现消息队列(多生产者,多消费者)?
    • 计算1-100的各个数的阶乘, 并且把各个数的阶乘放入到map中, 最后打印出来, 要求使用goroutine完成
      • 解法一:使用普通map结构,会出现map的并发写异常
      • 解法二: 使用mutex修饰map,避免并发写异常,main goroutine延时等待子goroutine的执行
      • 解法三: 使用mutex修饰map,避免并发写异常,使用channel控制main goroutine的退出
    • 统计1-100000 的数字中, 哪些是素数?
      • 解法一:使用传统的for循环,只使用一个协程判断各个数是否为素数
      • 解法二:使用goroutine和channel,使用多个协程并发判断各个数是否为素数
      • 参考文档
    • 习题
      • 参考文档
    • 打印平方数
      • 无限打印平方数
      • 打印0-100的平方数
    • 考点: channel的select操作
      • 相关知识点
        • channel的select操作遵循的原则:
      • 面试题: 下面代码会触发异常吗?请详细说明
        • 参考答案
      • 参考文档
    • 解释说明常见的并发模型及其实现原理
      • 进程
      • 线程
      • 协程
      • 基于回调的非阻塞/异步IO
    • 解释说明常见的并发实现模式?
      • 基于共享内存的并发模式
      • 基于消息传递的并发模式
    • Golang中常见的并发控制方式?
      • 通过channel实现并发控制
      • 通过sync包中的各种锁实现并发控制
      • 通过Context上下文实现并发控制
    • 解释说明goroutine的MPG调度模式?
    • Channel是同步的还是异步的?
    • 无缓冲和有缓冲通道之间有什么区别?
    • 无缓存channel的发送和接收是否同步?
    • 解释说明锁等待队列的运行机制,并结合管道的设计,理解管道设计的原理
    • 互斥锁/读写锁/死锁相关概念
      • 互斥锁
      • 读写锁
      • 死锁
        • 预防死锁
        • 避免死锁
        • 检测死锁
        • 解除死锁
      • 参考文档
    • Golang中的锁有哪些?
      • 互斥锁
      • 读写锁
      • sync.Map安全锁
      • 参考文档
    • go func() {} () 如何传入和传出参数?
      • go func() {} () 传入参数
    • 关于channel的两种不同的遍历方式
      • channel的遍历方式一: 先关闭,再遍历
      • channel的遍历方式二: 先遍历,再关闭
    • 遍历一个channel, 当channel为空&未关闭时, 该channel返回什么数据?
    • 遍历一个channel, 当channel已关闭&遍历到channel末尾时, 该channel返回什么数据?
    • Go如何在main goroutine结束前保证其他goroutine的顺利执行?
      • 协程同步面临的问题
      • 方案一: 利用sync.WaitGroup实现协程同步
      • 方案二: 利用管道实现同步,子协程结束后发送信号给主协程
      • 方案三: main goroutine延时等待其他goroutine的退出
      • 参考文档
    • 解释如下代码的含义?
    • channel的select操作遵循的原则:
  • 参考文档

sync.Once的作用和用法?

sync.Once: 只执行一次的代码,特别适合用于实现单例模式
go语言:sync.Once的用法
golang sync.Once 应用
Go语言——sync.Once分析

现有上亿行文件,想统计其中hello这个单词有多少个,如果用go实现,你有那些思路?

用两个goroutine分别打印奇数和偶数,输出1~1000

解题思路

使用两个channel(长度为1)和两个goroutine,分别是oddChannel/oddGoroutine和evenChannel和evenGoroutine;
oddGoroutine在从oddChannel接收到数据1后开始打印奇数;打印完毕后,向evenChannel写入数据1,通知evenGoroutine开始打印偶数;
evenGoroutine在从evenChannel接收到数据1后开始打印偶数;打印完毕后,向oddChannel写入数据1,通知oddGoroutine开始打印奇数;
为使奇数开始先打印,还需要一个startChannel,该channel从main goroutine接收数据1,在startChannel收到数据1后开始奇数的打印,奇数打印完后开始偶数的打印;
所有线程执行结束后,需要关闭各goroutine,子goroutine的管理使用sync.WaitGroup进行;

参考代码: print_odd_even_test.go Test_Print_Odd_Even_Interview_Exec

参考文档

第一次用 Golang 出面试题小记

交替打印数字和字母: 使用两个 goroutine 交替打印序列,一个 goroutinue 打印数字, 另外一个goroutine打印字母, 最终效果如下 12AB34CD56EF78GH910IJ 。

解题思路

使用两个channel(长度为1)和两个goroutine,分别是numChan/numGoroutine和letterChan/letterGoroutine;
numGoroutine在从numChan接收到数据1后开始打印数字;打印完毕后,向letterChan写入数据1,通知letterGoroutine开始打印字母;
letterGoroutine在从letterChan接收到数据1后开始打印字母;打印完毕后,向numChan写入数据1,通知numGoroutine开始打印数字;
为使数字开始先打印,还需要一个startChannel,该channel从main goroutine接收数据1,在startChannel收到数据1后开始数字的打印,数字打印完后开始字母的打印;
所有线程执行结束后,需要关闭各goroutine,子goroutine的管理使用sync.WaitGroup进行;

参考代码: interview_pipeline_print_num_letter_test.go

参考文档

Gopher面试中的Coding 交替打印数字和字母

习题

有四个线程1、2、3、4。线程1的功能就是输出1,线程2的功能就是输出2,以此类推…现在有四个文件ABCD。初始都为空。现要让四个文件呈如下格式:
A:1 2 3 4 1 2…
B:2 3 4 1 2 3…
C:3 4 1 2 3 4…
D:4 1 2 3 4 1…

考点

  • Go文件操作
  • Go channel 实现pipeline

参考代码: pipeline_print_to_file_test.go
测试时记得先创建好a.txt、b.txt、c.txt、d.txt文件

参看文档

go 面试题

如何实现消息队列(多生产者,多消费者)?

计算1-100的各个数的阶乘, 并且把各个数的阶乘放入到map中, 最后打印出来, 要求使用goroutine完成

解法一:使用普通map结构,会出现map的并发写异常

参考代码: factorial_test.go: TestFactorialWithMap

解法二: 使用mutex修饰map,避免并发写异常,main goroutine延时等待子goroutine的执行

参考代码:factorial_test.go: TestFactorialWithConcurrentMap

解法三: 使用mutex修饰map,避免并发写异常,使用channel控制main goroutine的退出

参考代码:factorial_test.go: TestFactorialWithConcurrentMapAndChannel

统计1-100000 的数字中, 哪些是素数?

解法一:使用传统的for循环,只使用一个协程判断各个数是否为素数

解法二:使用goroutine和channel,使用多个协程并发判断各个数是否为素数

intChan:存放输入数据的管道
primeChan:存放输出结果的管道

开启一个协程,专门负责向intChan中放入数据;
开启四个协程,专门负责从intChan中取出各个数据,并判断是否为素数,若是,则放入primeChan

参考代码: prime_test.go

参考文档

尚硅谷_韩顺平_Go语言核心编程:16.8.6 应用实例3

习题

在这里插入图片描述
参考代码:read_write_channel_test.go

参考文档

尚硅谷_韩顺平_Go语言核心编程:16.8.6 应用实例1

打印平方数

  1. 开启一个协程Counter,负责产生数字序列0, 1, 2…, 并将产生的数字序列发送到naturals通道
  2. 开启一个协程Squarer,负责从通道naturals中取出数字,计算平方数, 并将计算结果发送到squares通道
  3. 开启一个协程Printer,负责从通道squares中取出计算结果并打印

无限打印平方数

square_test.go Test_Square_Infinity

打印0-100的平方数

square_test.go Test_Square_Limit

考点: channel的select操作

相关知识点

channel的select操作遵循的原则:

  • select中只要有一个case能执行,则立即执行
  • select中有多个case可以执行时,按伪随机方式随机选择一个case进行执行
  • select中如果没有一个case可以执行,则执行default块

面试题: 下面代码会触发异常吗?请详细说明

func main() {
	runtime.GOMAXPROCS(1)
	int_chan := make(chan int, 1)
	string_chan := make(chan string, 1)
	int_chan <- 1
	string_chan <- "hello"
	select {
	case value := <-int_chan:
		fmt.Println(value)
	case value := <-string_chan:
		panic(value)
	}
}

参考答案

本题考察goroutine的select操作,代码本身没有问题,可以正常编译通过并执行。
但是,在select时,当case value := <-string_chan先满足条件可以执行,或者case value := <-int_chan和case value := <-string_chan两者都满足条件但在随机选择时选中case value := <-string_chan分支时,会触发panic()语句的执行,从而触发异常

参考文档

Go面试题答案与解析 5、下面代码会触发异常吗?请详细说明

解释说明常见的并发模型及其实现原理

  • 进程

进程是操作系统层面支持的并发模型,同是也是开销最大的一种并发模型。进程是系统进行资源分配的最小单位。

  • 线程

线程也是操作系统层面支持的并发模型,开销比进程小,并且线程依赖于进程而存在。线程是系统调度的最小单位。

  • 协程

协程,也被成为轻量级线程,依托于线程而存在,开销比线程更小,同时能创建比进程和线程更多的数量。目前支持协程的语言包括:Golang、Python、Erlang

  • 基于回调的非阻塞/异步IO

简单理解为基于事件的调度模型,在发生事件时,执行相应回调函数。目前在Node.js中有很好的实践。

解释说明常见的并发实现模式?

并发,说到底难点在于对共享变量访问控制,通常有以下两种实现模式:

  • 基于共享内存的并发模式

基于锁机制,线程在操作共享变量时,通过对共享变量加锁,实现对共享变量的独占式使用;在使用完毕后释放锁,以便其他线程使用

  • 基于消息传递的并发模式

基于消息机制,线程在收到消息后开始操作共享变量,并在操作完毕后传出消息,通知其他线程对共享变量进行操作

Golang中常见的并发控制方式?

  • 通过channel实现并发控制

concurrent_control_mode_test.go Test_Concurrent_Control_Mode_By_Channel

  • 通过sync包中的各种锁实现并发控制

concurrent_control_mode_test.go Test_Concurrent_Control_Mode_By_WaitGroup

  • 通过Context上下文实现并发控制

concurrent_control_mode_test.go Test_Concurrent_Control_Mode_By_Context

解释说明goroutine的MPG调度模式?

Golang中的协程为一种轻量级的线程,这些协程的执行最终还是依赖于操作系统内核线程的执行。Golang中goroutine的调度器主要有4个重要组成部分,分别是G、P、M、Sched:

  • G,goroutine,内核线程要执行的协程,可以理解为内核线程需要调度执行的任务
    G代表一个goroutine对象,每次使用go关键字开启一个goroutine时,都会创建一个G对象,该对象中包含goroutine的调用栈信息、重要的调度信息(例如channel等)
  • P,processor,执行内核线程的CPU
    P代表goroutine执行的上下文,主要用于衔接M和G;P的数量是通过GOMAXPROCS()函数进行设置的,表示实际并发执行的Goroutine数量(真正的并发度,和系统的CPU核数有关)
  • M,work thread,实际执行的内核线程
    M代表一个实际执行的内核线程;每创建一个M,都会有一个对应的内核线程被创建;G的执行最终都是由执行M来实现的。
  • Sched
    Sched是调度实现中使用的数据结构,大多数需要的信息都已经存储在了结构体M、P和G中,Sched结构体只是一个壳。Sched结构体中的Lock是必须的,如果M或P等做一些非局部的操作,他们一般需要先锁住调度器。

每次调用go关键字创建一个goroutine时,相当于向内核中提交了一个任务,这些任务的调度执行遵循如下流程(GPM调度模式可理解为线程池的调度模式):

  1. 创建一个G对象(任务),加入到本地队列或者全局队列;
  2. 如果还有空闲的P,则创建一个M(CPU要跑满,但一个CPU只能同时调度一个内核线程);
  3. M会启动一个内核线程,循环执行能找到的G任务;
  4. G任务的执行顺序是,先从本地队列找,本地没有则从全局队列找;
  5. 以上的G任务的执行是按照队列顺序执行的;

G是顺序执行的是不是有点奇怪,跟我们实际了解到的情况不一样。原因是,启动的时候,会专门创建一个线程sysmon,用来监控和管理,在内部是一个循环:

  1. 记录所有P的G任务计数schedtick,schedtick会在每执行一个G任务后递增;
  2. 如果检查到schedtick一直没有递增,说明这个P一直在执行同一个G任务,如果超过一定的时间(10ms),就在这个G任务的栈信息里面加一个标记;
  3. 然后这个G任务在执行的时候,如果遇到非内联函数,就会检查一次这个标记,然后中断自己,把自己加入到队列末尾,执行下一个G;
  4. 如果没有遇到非内联函数调用的话,那就惨了,会一直执行这个G任务,直到它自己结束;

Go全栈面试题(2) -Go进阶面试题

Channel是同步的还是异步的?

无缓冲和有缓冲通道之间有什么区别?

无缓存channel的发送和接收是否同步?

解释说明锁等待队列的运行机制,并结合管道的设计,理解管道设计的原理

互斥锁/读写锁/死锁相关概念

互斥锁

读写锁

死锁

预防死锁

避免死锁

检测死锁

解除死锁

参考文档

Go全栈面试题(2) -Go进阶面试题 互斥锁,读写锁,死锁问题是怎么解决。

Golang中的锁有哪些?

Golang中的三种锁包括:互斥锁,读写锁,sync.Map安全的锁

互斥锁

读写锁

sync.Map安全锁

参考文档

Go全栈面试题(2) -Go进阶面试题 说下Go中的锁有哪些?

go func() {} () 如何传入和传出参数?

go func() {} () 传入参数

go_func_parameter_test.go
在第一个()处作传入参数类型声明,在第二个()处出入实际参数

关于channel的两种不同的遍历方式

channel的遍历方式一: 先关闭,再遍历

channel_loop_test.go TestChannelLoop_1_AfterClose
在不明确知道channel长度时, 需要线先进行channel的关闭, 然后才能进行channel的遍历

channel的遍历方式二: 先遍历,再关闭

channel_loop_test.go TestChannelLoop_2_BeforeClose
在明确知道channel长度时, 可以先使用普通的for循环进行channel的遍历, 在遍历后在进行channel的关闭

遍历一个channel, 当channel为空&未关闭时, 该channel返回什么数据?

channel底层实现为一个队列,read & write可视为两个协程并发的操作这个队列
新建一个channel时,channel为空并且未关闭,readFlag=false & writeFlag=true,该channel等待goroutine写入数据;
当goroutine写入数据后,channel不为空并且未关闭,readFlag=true & writeFlag=false,该channel等待goroutine读取数据;
当goroutine读取完数据后,channel再次为空并且未关闭,readFlag=false & writeFlag = true, 等待goroutine写入数据;
如此,便实现两个goroutine对chan的读写操作

但是,当该channel为空并且未关闭时,readFlag=false & writeFlag=true,这个时候进行read操作便会因为并发修改channle而导致deadlock异常的出现

因此,在遍历channel之前,一定要确保channel已关闭,否则会出现deadlock问题

遍历一个channel, 当channel已关闭&遍历到channel末尾时, 该channel返回什么数据?

系统数据类型的默认值.
bool: false
int: 0

Go如何在main goroutine结束前保证其他goroutine的顺利执行?

协程同步面临的问题

main goroutine在子goroutine还未执行完成时便退出, 导致子goroutine无法顺利执行
exit_main_test.go TestExitMain_Exception
例如如上测试案例: Goroutine 1和Goroutine 2很可能无法得到执行, 因为main goroutine退出会导致子goroutine没有时间执行

方案一: 利用sync.WaitGroup实现协程同步

exit_main_test.go TestExitMain_1_WaitGroup
sync.WaitGroup内部实现了一个计数器,用来记录未完成的操作个数,它提供了三个方法:

  • Add()用来添加计数。
  • Done()用来在操作结束时调用,使计数减一。
  • Wait()用来等待所有的操作结束,即计数变为0,该函数会在计数不为0时等待,在计数为0时立即返回。

Go并发:利用sync.WaitGroup实现协程同步

方案二: 利用管道实现同步,子协程结束后发送信号给主协程

exit_main_test.go TestExitMain_2_Channel

方案三: main goroutine延时等待其他goroutine的退出

exit_main_test.go TestExitMain_3_Sleep

参考文档

Go全栈面试题(2) -Go进阶面试题 主协程如何等其余协程完再操作? & 如何等待所有goroutine的退出?

解释如下代码的含义?

done := make(chan struct{})
done <- struct{}{}

channel的select操作遵循的原则:

  • select中只要有一个case能执行,则立即执行
  • select中有多个case可以执行时,按伪随机方式随机选择一个case进行执行
  • select中如果没有一个case可以执行,则执行default块

参考文档

go获取协程(goroutine)号

你可能感兴趣的:(Golang)