1. 知识点
简单介绍channel
- 对nil的channel读写会如何?
答: 读写都会造成阻塞
- 对已关闭的channel操作会有什么特点?
答:读,无缓存值
→ 读出0值; 读,有值
→ 正常读; 写
→ panic; 关闭
→ panic;
- channel的原理、概念、底层实现?
答:通过makechan创建的hchan 分为有缓冲和无缓冲的通道
有缓冲与无缓冲的区别
1、无缓冲的管道是同步的,必须使用协程接收,否则会导致死锁;
2、有缓冲是异步的,但是如果缓冲区已满,发送消息会阻塞,如果缓冲区为空,接收消息会阻塞;如何判断一个channel已关闭
1、往一个已经关闭的管道写数据,panic,路子比较野
2、使用_, ok := ch结合select来实现,ok如下情况,
true:读到数据,并且通道没有关闭。
false:通道关闭,无数据读到
2. channel的应用场景
channel适用于数据在多个协程中流动的场景,有很多实际应用:
- 任务定时
比如超时处理:
select {
case <-time.After(time.Second):
定时任务
select {
case <- time.Tick(time.Second)
解耦生产者和消费者
可以将生产者和消费者解耦出来,生产者只需要往channel发送数据,而消费者只管从channel中获取数据。控制并发数
以爬虫为例,比如需要爬取1w条数据,需要并发爬取以提高效率,但并发量又不过过大,可以通过channel来控制并发规模,比如同时支持5个并发任务:ch := make(chan int, 5)for _, url := range urls {go func() {ch <- 1worker(url)<- ch}}协程之间的通信. 通过channel传递数据,实现通信
协程之间的同步. 通过控制channel读写数据的时序,实现不同协程之间的同步
实现工作池. 通过channel实现对工作池任务的分配和处理
实现信号量. 设置1大小的缓冲区,模拟加锁和解锁
实现超时机制. channel结合select实现超时任务.
go里面只有值拷贝
- 为什么不要使用大量的协程?
1、过多会占有太多的cpu资源和内存,可能使系统资源耗尽
2、因为GMP,M和P都是有数量限制的,如果调度队列过长,也会影响性能;
3、频繁GC也会影响性能;
使用goroutine可以帮助提高程序的并发性和性能,但是过度使用goroutine会带来一些问题,例如:
内存占用增加,因为每个goroutine都需要占用一定的内存
过多的goroutine会导致CPU上下文切换频繁,影响程序性能
如果goroutine没有正确的管理,可能会导致资源泄漏或死锁为了优化这些问题,可以考虑以下方法:
确定适当的goroutine数量,避免过度使用goroutine。
使用有限的goroutine池,以限制goroutine的总数,并避免内存占用过多。
优化goroutine的调度,以减少CPU上下文切换的次数。
使用通道和其他同步原语来避免竞争条件和死锁。
https://www.jianshu.com/p/1a50330adf1b
- 读写锁如何实现的?
读写锁的规则:
RWMutex在读锁占用的是时候,会阻塞写,不会阻塞读;
RWMutex在写锁占用的时候,无论是读还是写都会被阻塞;
读写锁的底层实现也是基于互斥锁来实现的。
如果没有writer请求进来,则每个reader开始后只是将readerCount增1,完成后将readerCount减1,整个过程不阻塞,这样就做到“并发读操作之间不互斥”;
当有writer请求进来时首先通过互斥锁阻塞住新来的writer,做到“并发写操作之间互斥”;
然后将readerCount改成一个很小的值,从而阻塞住新来的reader;
记录writer进来之前未完成的reader数量,等待它们都完成后再唤醒writer;这样就做到了“并发读操作和写操作互斥”;
writer结束后将readerCount置回原来的值,保证新的reader不会被阻塞,然后唤醒之前等待的reader,再将互斥锁释放,使后续writer不会被阻塞。
主要是两个计数器,count正常读请求就是+1-1,有写锁的话就会变为一个很小的负数防止后来的读请求执行,然后用readerWaiter记录之前的读请求数量,等待执行完,唤醒写请求,然后再解写锁给还原回去,再继续执行之后的读请求等
- 深拷贝与浅拷贝
深拷贝: 相当于重新克隆了一份
浅拷贝: 相当拷贝了一份引用,地址还是指向原有的地址,修改会对原有值有影响
深拷贝 = 值拷贝 不互相影响
浅拷贝 = 引用拷贝 互相影响
- make和new的区别
make
只能用来创建slice、map、channel, 而new
可以用来创建任何类型。
make
返回一个有初始值的实例,new
创建一个没有任何数据的类型,返回指针。
- 数组如何实现用下标访问任一元素?
1、数组是线性表;
2、数组的元素在内存中存放是连续的,利用下班访问任意元素可以通过寻址公式:
a[i]_address = base_address + i* data_type_size
其中base_address表示首地址,data_type_size 表示数组中每个元素的大小
一般来说可以用首地址+数据类型大小*偏移量计算得到