go并发面经

go语言中并发安全性是什么?

多个 goroutine同时访问共享数据时,程序能够正确地执行而不会导致数据竞争或其他并发问题。
Go 提供了一些机制来确保并发安全性:互斥锁、读写锁、channel

互斥锁(Mutex)

介绍一下互斥锁的原理

互斥锁的结构体字段:
state:

  • mutexLocked 持有锁标记
  • mutexWoken 唤醒标记
  • mutexStarving 饥饿标记
  • mutexWaiters 阻塞等待的waiter数量

same:信号量,用来控制等待goroutine的阻塞、休眠和唤醒操作

正常模式和饥饿模式:
正常模式:

  • 先入先出,被唤醒的 waiter 和新来的 goroutine 进行竞争
  • 唤醒的 waiter获取不到锁,插入到队列的前面。新来的 goroutine获取不到锁,插入到队列尾部
  • waiter获取不到锁超过1毫秒,进入饥饿模式
    饥饿模式:
  • 锁交给队列最前面的waiter
  • 新来的goroutine不会获取锁,直接插入队列尾部
  • waiter 是队列中的最后一个了或者waiter 的等待时间小于 1 毫秒,进入正常模式
四个阶段
  • 初版
    • 使用一个flag字段标记是否持有锁
  • 给新人机会
    • 唤醒的goroutine和新来的goroutine竞争锁
  • 给新人多一点机会
    • 唤醒的goroutine或者新来的goroutine首次获取不到锁,会自旋(spin),尝试一定的自旋次数后,再执行回原来的逻辑
  • 解决饥饿
    • 有可能每次都被新来的goroutine抢到锁,极端情况下,等待中的goroutine一直获取不到锁
    • 等待时间超过1ms进入饥饿模式
初版的实现原理
  • key和sema字段,
  • key
    • 0:锁未被持有
    • 1:锁被持有,还没有等待者
    • n:锁被持有,还有n-1个等待着
  • sema:等待者队列使用的信号量
  • 使用key字段标记是否持有锁,请求锁到来,通过原子操作CAS加1,释放锁通过原子操作减1
  • 如果锁没有被其他goroutine持有,直接获取到锁,否则使用信号量将自己休眠,等待锁释放的时候,信号量会唤醒
自旋(spin)具体是做些什么事情
  • 循环检查锁的状态,如果锁的状态释放了,就竞争锁
  • 自旋一定的次数
  • 如果锁被持有时间很短,那么避免了线程切换的开销

读写锁(RWMutex)

介绍一下你对读写锁的了解

1、某一时刻只能由任意数量的reader(读锁)持有,或者是只被单个的writer(写锁)持有
2、写优先

  • 请求的writer到来,如果已经有一些reader请求了锁的话,writer会等待已经存在的reader都释放锁之后才获取到锁,后面来的新reader要等writer执行完后才会获得锁。

3、结构体:

  • w // 互斥锁解决多个writer的竞争
  • writerSem // writer信号量
  • readerSem // reader信号量
  • readerCount // 记录当前 reader 的数量(以及是否有 writer 竞争锁)
  • readerWait // 记录 writer 请求锁时需要等待 read 完成的 reader 的数量
  • 一个常量:rwmutexMaxReaders = 1<<30 //定义了最大的 reader 数量
    4、易错场景
  • 不可复制
  • 重入导致死锁
  • 释放未加锁的 RWMutex
针对写优先,比如说现在有一个写锁已经加锁了,这个时候有一个读锁过来,又有几个写锁过来,他们几乎是同时到达的。那么,第二个到来的读写跟后面几个写锁谁先去加锁?
  • 虽然是写优先,但是是读锁先加锁
  • 写锁释放锁后,会先对现在等待队列里面的读锁先一批全部唤醒加锁
  • 写->读->写->读
    • 只要是现在写锁持有锁
    • 后面不管是读锁先到还是写锁先到
    • 写锁释放之后,都会先唤醒一批读锁
读写锁适合什么场景

读多写少

说一下sync包下面并发原语

  • Mutex
  • RWMutex
  • WaitGroup
  • Cond
  • Once
  • Map
  • Pool

WaitGroup

介绍一下WaitGroup

等待一组goroutine完成执行的同步原语
方法:

  • Add:用来设置 WaitGroup 的计数值
  • Done:用来将 WaitGroup 的计数值减 1,其实就是调用了 Add(-1)
  • Wait:调用这个方法的 goroutine 会一直阻塞,直到 WaitGroup 的计数值变为 0

结构体:

  • noCopy
    • 不能在第一次使用之后复制使用
  • state1
    • waiter数
    • 计数值
    • 信号量

易错场景:

  • 计数器设置为负值
    • 调用 Add 的时候传递一个负数
    • 调用 Done 方法的次数过多,超过了 WaitGroup 的计数值
  • 不期望的 Add 时机。 并发Wait 和 Add,会出现 panic。
有实际使用过吗?
  • 场景:起3个协程跑API接口获取数据,等3个协程都获取完数据之后再往下执行
  • 主goroutine,声明WaitGroup,调用Add(3),起3个协程跑API
  • 每个协程执行完之后,调用Done()方法
  • 主goroutine,调用Wait()方法等待3个协程执行完

map

介绍一下map

无序的,键值对,并发安全
方法:

  • Store
  • Load
  • Delete
  • Range

Key的要求:

  • K 必须是可比较的
  • 不可比较类型:slice、map、函数、struct包含slice也不可以当key
slice、map、函数为什么不可以比较?
  • slice:
    • 引用类型,底层是数组
    • 底层的数组可能不一样
    • 或者有可能会发生扩容,扩容之后底层的数组就会不一样
    • 比较维度比较复杂,所以就不可以比较
  • map和函数
    • map可以包含切片
    • 函数可以把切片当做参数传递
    • 因为包含了切片所以不可以比较
slice的底层结构

数组指针、长度、容量

slice扩容机制
  • 当长度小于1024时,每次扩容会将容量翻倍。即新容量为原容量的两倍。
  • 当长度大于等于1024时,每次扩容会将容量增加25%。即新容量为原容量的1.25倍。
  • 扩容后,会创建一个新的底层数组,并将原有的元素复制到新的底层数组中
slice当前的容量为256,新增元素是当前容量的两倍(512),它是怎么扩容的?
  • 按照扩容机制,多次扩容
  • 第一次扩容到512,512容量还不够就会扩容到1024,还不够就会扩容到1024*1.25=1280
为什么sync.map是线程安全的?怎么实现线程安全的?
  • 看底层源码或者自己百度

go里面有哪些类型是线程安全的?

sync包下面的并发原语、atomic、channel

Once

介绍一下Once

用来执行且仅仅执行一次动作,常常用于单例对象的初始化场景
方法:

  • Do:只有第一次调用会执行

结构体:

  • done uint32 标记是否已经执行过Do方法
  • m Mutex 保证只有一个 goroutine 执行Do方法
为什么有个done做标识了,还要加多一个互斥锁?
  • 为了做双重检测
  • done是标记是否已经执行过Do方法的参数f函数了
  • 互斥锁保证只有一个 goroutine 执行Do方法
Once有使用过吗?

使用过,单例模式初始化

Pool

介绍一下Pool

来缓存一组可独立访问的临时对象,避免反复创建销毁带来的性能损耗
方法:

  • New:创建新的元素
  • Get:取走元素
  • Put:返回元素

结构体:

  • noCopy
  • local:用来存储当前主要的空闲可用的元素
  • localSize
  • victim:用来存储空闲的元素。垃圾回收时,把 victim 中的对象移除,然后把 local 的数据给 victim,local 就会被清空
  • victimSize
  • New

坑:

  • 内存泄露
  • 内存浪费
主要是为了避免哪个方面的性能损耗?
  • 内存分配开销: 在高并发的情况下,频繁地进行内存分配可能导致性能下降。
  • 垃圾回收(GC)压力: 在大量对象被频繁创建和销毁的情况下,垃圾回收器可能会变得繁忙,导致应用程序的性能下降。

Cond

介绍一下Cond

等待某个条件的一组 goroutine,等条件变为 true 的时候,其中一个 goroutine或者所有的 goroutine 都会被唤醒执行
方法:

  • Broadcast:唤醒等待队列中的所有waiter
  • Signal:唤醒等待队列中的第一个waiter
  • Wait :
    • 把调用者放到等待队列中并阻塞,直到被Broadcast或者Signal唤醒并从等待队列中删除
    • 必须要持有锁(一般Mutex 或者 RWMutex)

结构体:

  • L 绑定的锁
  • notifyList 等待 / 通知的队列
  • copyChecker 辅助结构,检查 Cond 是否被复制使用
Cond结构体里面的锁是用来干什么的?
  • 用来保护条件变量的状态(调用wait时必须加锁,也做条件变量检测)
  • 保护等待队列的出队入队

Atomic(原子操作)和锁的区别是什么?

  • 原子操作的粒度更小: 原子操作通常用于对单个变量进行原子操作,而锁通常用于对一组操作或代码块进行同步控制。
  • 原子操作无需显式加锁: 原子操作是由硬件提供支持的,并不需要程序员显式地加锁和解锁,而锁需要显式地进行加锁和解锁,容易引入死锁和竞态条件。
  • 锁提供更灵活的同步控制: 锁可以用于保护一段复杂的代码块,确保在同一时刻只有一个线程可以执行该代码块。

如何保证go内建的map是并发安全的,直接使用锁粒度太大,还有其他方法吗?

分片锁:分片锁是一种通过将数据划分成多个片段(分片),为每个片段分配一个独立的锁的方式,以提高并发性能

Channel

介绍一下channel

通过通信共享内存,而不是通过共享内存而实现通信
基本用法:

  • 发送数据
  • 接收数据
  • 关闭chan

结构体:

  • qcount:循环队列元素的数量
  • dataqsiz:循环队列的大小。
  • buf:循环队列的指针。
  • elemtype 和 elemsize:chan 中元素的类型和 size。
  • sendx:处理发送数据的指针在 buf 中的位置。
  • recvx:处理接收请求时的指针在 buf 中的位置。
  • recvq:接收者等待队列。
  • sendq:发送者等待队列。
  • lock:互斥锁

常见错误:

  • panic
    • close 为 nil 的 chan;
    • send 已经 close 的 chan;
    • close 已经 close 的 chan。
  • goroutine 泄漏
    • 缺少接收端或者发送端,或者提前结束接收端或者发送端没有close channel

无缓冲channel:

  • 无缓冲的通道又称为阻塞的通道
  • 使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

有缓冲channel:

  • 通道的容量填满才会阻塞
  • 通道为空时,接收数据会阻塞

单向通道:

  • 函数中只能发送或只能接收
  • 在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的
Channel内存泄露,具体是泄露什么东西?

goroutine泄露。比如goroutine里面有一个没有缓存的channel接收端,如果发送端因为其他原因没有执行到,或者是提前结束了发送端没有结束channel,会导致goroutine一直阻塞,且无法被gc回收。

单向channel可以赋值给双向channel吗?

不可以。单向不可以赋值给双向,双向可以赋值给单向

读取一个有数据的channel且是关闭的,会发生什么?

会接收到channel里面的数据,直接channel里面没有数据,返回channel对应元素的零值

读取一个有数据的channel且是关闭的,接收两个返回参数,两个参数分别代表是什么?

第一个参数:通道中的值,如果通道还有数据,那么就是通道中的下一个值。如果通道已经空了,返回通道元素的零值。
第二个参数:是否成功读取,如果成功读取了一个值,该返回值为 true;如果通道已经关闭且已经没有数据可读,返回值为 false。

select是用来做什么的?

用来监听一个或者多个channel,直到其中一个channel ready

select和switch有什么区别?
  • select:用来监听一个或者多个channel
  • switch:流程控制,多个表达式的比较
  • 两者没有任何关系,就是长得比较像而已

分布式并发原语有没有了解过?

分布式并发原语etcd

  • Leader 选举
    • 选举
    • 查询
    • 监控
  • 互斥锁
    • 同一时刻,只允许其中的一个节点持有锁
    • Locker
      • 类似于 Go 标准库中的 sync.Locker 接口,提供了 Lock/UnLock 的机制,基于 Mutex 实现
        • Mutex:提供了Lock/UnLock和查询 Mutex 的key 的信息的功能
        • 读写锁:和标准库的读写锁的功能是一样的
  • 队列
    • 分布式队列
      • 多读多写的队列,可以启动多个写节点和多个读节点
    • 优先级队列
      • 和队列类似,写入元素需要提供 uint16 整数优先级,优先级高优先出队。
  • 栅栏
    • Barrier:分布式栅栏
      • 持有 Barrier 的节点释放了它,所有等待这个 Barrier 的节点才会继续执行
    • DoubleBarrier:计数型栅栏
      • 初始化计数型栅栏时,就必须提供参与节点的数量,当这些数量的节点都 Enter 或者 Leave 的时候,这个栅栏就会放开
  • STM(事务)
    • 简化多个key的操作,并且提供事务功能(要么全成功,要么全失败)
    • 方法:Get、Put、Receive 和 Delete

go里面是怎么保证并发的读写顺序的?

内存模型:并发环境中多goroutine 读相同变量的时候,变量的可见性条件
happens-before:

  • goroutine 内部,程序的执行顺序和它们的代码指定的顺序是一样的
  • go语言中保证的happens-before关系
  • init 函数
    • init函数一定在当前包的任何初始化代码之前执行
  • goroutine
    • 启动 goroutine 的 go 语句的执行,一定 发生在此 goroutine 内的代码之前执行
    • 根据此规则,go 语句传入的参数是一个函数执行的结果,那么,这个函数一定先于 goroutine 内部的代码被执行。(参数结果从传入的时候就固定了)
  • Channel
    • 对 Channel 的第n个发送操作,一定发生在第n个接收操作之前
    • close Channel 的操作,一定发生在从 Channel中读取出零值之前。
    • 对无缓冲的Channel的读取操作,一定发生在此Channel的发送操作之前
  • Mutex/RWMutex
    • 解锁操作一定发生在下次上锁操作之前
  • WaitGroup
    • Wait 方法等到计数值归零之后才返回
  • Once
    • once.Do(f) 调用,函数 f 一定会在任何 Do 方法返回之前执行
  • atomic
    • Store 一定在Load之前执行,但是太过于复杂,现阶段还是不要使用 atomic 来保证顺序性。

golang 协程是否可以无限制的创建?

在理论上,是可以无限制地创建

无线创建协程会出现什么样的问题?

  • 内存消耗: 每个协程都需要一定的内存来存储其状态和栈信息。如果无限制地创建协程,可能会导致大量的内存消耗,最终耗尽可用内存。
  • 调度开销: 每个协程都需要进行调度和上下文切换。如果存在大量的协程,调度和切换的开销可能变得显著,导致系统性能下降。

线程和go的协程的区别是什么?

线程:

  • 是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
  • 一个线程上可以跑多个协程,协程是轻量级的线程

协程:

  • 独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

区别

  • 内存占用:线程的内存占用比协程大。通常情况,线程创建栈大小1MB,协程创建栈大小2KB\
  • 切换成本:
    • 协程是在用户态切换完成的,这个过程较为轻量
    • 线程是在内核态切换完成的,这个过程较为耗时
    • 还有寄存器的数量也不一致

Context

介绍一下context

上下文信息传递,还提供了超时(Timeout)和取消(Cancel)的机制
1个接口,4个具体实现,6个函数:

  • context.Context是个接口,定义了4个方法
    • Deadline:返回 被取消的截止日期
    • Done:返回一个 Channel 对象。在 Context 被取消时,此 Channel 会被 close
    • Err:返回 Done 被 close 的原因
    • Value:返回此 ctx 中和指定的 key 相关联的 value
  • 4个具体实现
    • emptyCtx:本质是个整形(int),对接口的实现只是简单的返回nil,false
    • cancelCtx:可取消的context
    • timerCtx:在cancelCtx基础上,封装了一个定时器和一个截止时间
    • valueCtx:给context附加一个键值对信息
  • 6个函数
    • Background:
      • 返回一个非 nil 的、空的 Context
      • 一般用在主函数、初始化、测试以及创建根Context 的时候
      • 类型是 emptyCtx
    • TODO
      • 返回一个非 nil 的、空的 Context
      • 当你不清楚是否该用 Context,或者目前还不知道要传递一些什么上下文信息的时候,就可以使用这个方法。
      • 类型是 emptyCtx
    • WithValue
      • 基于 parent Context 生成一个新的 Context,保存了一个 key-value 键值对
      • 类型是 valueCtx
    • WithCancel
      • 返回 parent 的副本,只是副本中的 Done Channel 是新建的对象
      • 类型是 cancelCtx。
    • WithDeadline
      • 返回一个 parent 的副本,并且设置了一个截止时间
      • 类型为是timerCtx
    • WithTimeout
      • 和 WithDeadline 一样,只不过设置的是超时时间
      • 类型为是timerCtx
gin框架的context和go内建的context有什么区别
  • gin的context
    • 通过扩展 context.Context 接口而来的
    • 包含了很多与 HTTP 请求相关的信息
    • 主要用于 HTTP 请求的处理
  • 内建context
    • 用于处理上下文信息的通用接口
内建的context用来做什么?

上下文信息传递,还提供了超时(Timeout)和取消(Cancel)的机制

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值