Go-goroutine & channel

现有上亿行文件,想统计其中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

习题

Go-goroutine & channel_第1张图片

参考代码: 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中goroutine的调度依赖于M(work thread)、P(processor)、G(goroutine)三个基础组件,故又被称为MPG调度模式

MPG模式的基础组件说明

  • G,goroutine
    G代表一个Goroutine,每次使用go关键字开启一个Goroutine时,都会创建一个G对象(该对象中包含Goroutine的调用栈信息、重要的调度信息(例如channel等))
  • P,processor
    P代表Goroutine执行的上下文,主要用于衔接M和G;P的数量是通过GOMAXPROCS()函数进行设置的,表示实际并发执行的Goroutine数量(真正的并发度,和系统的CPU核数有关)
  • M,work thread
    M代表一个实际执行的内核线程;每创建一个M,都会有一个对应的内核线程被创建;G的执行最终都是由执行M来实现的。

MPG模式的goroutine调度流程

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的退出?

解释如下代码的含义?

func TestBank(t *testing.T) {
	// 如下代码段的含义是?
	// Deposit [1..1000] concurrently.
	var n sync.WaitGroup
	for i := 1; i <= 1000; i++ {
		n.Add(1)
		go func(amount int) {
			bank.Deposit(amount)
			n.Done()
		}(i)
	}
	n.Wait()

	if got, want := bank.Balance(), (1000+1)*1000/2; got != want {
		t.Errorf("Balance = %d, want %d", got, want)
	}
}

解释如下代码的含义?

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

相关知识点

channel的select操作遵循的原则:

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

参考文档

go获取协程(goroutine)号

你可能感兴趣的:(Golang)