go并发编程相关

本文详细介绍了Go语言中的并发编程关键概念,如Mutex的使用、WaitGroup的计数管理、RWMutex的读写锁、Cond条件变量、sync.Once单例以及channel的通信机制,还包括原子操作和避免常见错误的方法。
摘要由CSDN通过智能技术生成

go并发编程相关

  1. Mutex的基本使用方法:

func main() {
	// 封装好的计数器
	var counter Counter
	var wg sync.WaitGroup
	wg.Add(10)
	// 启动10个goroutine
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			// 执行10万次累加
			for j := 0; j < 100000; j++ {
				counter.Incr() // 受到锁保护的方法
			}
		}()
	}
	wg.Wait()
	fmt.Println(counter.Count())
}

// Counter 线程安全的计数器类型
type Counter struct {
	CounterType int
	Name        string
	mu          sync.Mutex
	count       uint64
}

// Incr 加1的方法,内部使用互斥锁保护
func (c *Counter) Incr() {
	c.mu.Lock()
	c.count++

	c.mu.Unlock()
}

// Count 得到计数器的值,也需要锁保护
func (c *Counter) Count() uint64 {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.count
}

  1. Mutex锁的实现机制?
    Mutex结构体:
// Mutex 互斥锁的结构,包含两个字段
type Mutex struct {
	key  int32 // 锁是否被持有的标识  0--没被持有 1--被持有了,没有等待者 n--还有n-1个等待者
	state int32 // 信号量专用,用以阻塞/唤醒goroutine
}

Mutex结构体,
key字段表示锁持有的状态,

state 一个字段包含多个意义,这样可以通过尽可能少的内存来实现互斥锁。
mutexLocked: 字段第一位(最小的一位)表示这个锁是否被持有,
mutexWoken: 第二位代表是否有 唤醒的 goroutine,
mutexWaiters: 剩余的位数代表的是等待此锁的 goroutine 数。
在这里插入图片描述
Mutex相比从传统互斥锁的进步:

  1. 多给新goroutine执行机会.
    新来的 goroutine先进行争抢锁, 争抢不到再进入等待队列, 打破了先到先得的逻辑.
    争取让正在执行的协程继续执行, 这样可以减少协程运行状态的切换, 提高运行效率

  2. 增加了自旋重试
    新来的 goroutine争抢不到锁, 会进行4次重试, 如果正好获取到上一个协程释放到的锁,
    这样也避免了协程状态切换, 提高效率

  3. 解决饥饿
    由于新来的线程处于正在执行状态, 在争取锁时更容易获取到, 这样等待队列中的协程
    可能长时间获取不到锁. 为避免这种情况, Mutex引入饥饿模式.

     饥饿模式: 
     	如果队列中的任务最大等待时间超过1ms, 那么进入饥饿模式. 
     	饥饿模式会将上一个协程释放的锁, 直接交给队列头部等待的协程. 新来的协程直接加
     	入队列尾部
    

注意:
Unlock() 方法可以被任意的 goroutine调用释放锁 ! ! !
即使是没有持有这个互斥锁的 goroutine, 也可以进行这个操作.

因为 Mutex本身并没有包含持有这把锁的 goroutine的信息, 所以, Unlock也不会对此进行检查, Mutex的这个设计保留至今.

所以, Mutex一定坚持 "谁申请, 谁释放"的原则, 一般申请和释放配对使用

  1. 互斥锁常见的出错场景:
    1. lock / unlock不是成对出现
    2. copy 已使用的 Mutex
    3. 重入 Mutex 不是可重入的锁。
    4. 死锁

  2. RWMutex 读写锁的实现原理
    读写锁方法:
    Lock / Unlock: 写操作加锁
    RLock / RUnlock: 读操作加锁


type RWMutex struct {
    w Mutex // 互斥锁解决多个writer的竞争
    writerSem uint32 // writer信号量
    readerSem uint32 // reader信号量
    readerCount int32 // reader的数量
    readerWait int32 // writer等待完成的reader的数量
}

reader 计数加 1。readerCount 怎么还可能为负数呢?
reader 计数加 1。readerCount 可以为负数呢?
因为 readerCount 字段有双重含义:

没有 writer 竞争或持有锁时,readerCount 就是普通 reader 的计数;
如果有 writer 竞争锁或者持有锁时,那么,readerCount标记为负数, 表示持有写锁

调用 RUnlock时, Reader 的计数减去 1,
因为 reader 的数量减少了一个。

如果它是负值, 就表示当前有 writer 竞争锁,
在这种情况下,还会调用 rUnlockSlow 方法,检查是不是 reader 都释放读锁了,
如果读锁都释放了,那么可以唤醒请求写锁的 writer 了。

当一个或者多个 reader 持有锁的时候,竞争锁的 writer 会等待这些 reader 释放完,才可能持有这把锁。

  1. 读写锁出现错误的场景:
    1. 不可复制
    2. 不支持重入
    3. 释放未加锁的RWMutex

  2. waitGroup的实现原理?

type WaitGroup struct {
    // 避免复制使用的一个技巧,可以告诉vet工具违反了复制使用的规则
    noCopy noCopy

    // 64bit(8bytes)的值分成两段,高32bit是计数值,低32bit是waiter的计数
    // 另外32bit是用作信号量的
    // 因为64bit值的原子操作需要64bit对齐,但是32bit编译器不支持,所以数组中的元素在不同的
    // 总之,会找到对齐的那64bit作为state,其余的32bit做信号量
    state1 [3]uint32
}

在这里插入图片描述
waitgroup的三个主要方法:
1.Add():设置waitgroup总数
2.Done(): waitgroup计数-1, 当计数减为0时候, 调用Wait()方法的协程恢复执行
3.Wait(): 调用者处于阻塞, 直到waitgroup计数减为0

  1. waitGroup常见的错误?
    1. 计数器设置为负值
    2. 没有等所有计数 Done()操作执行完, 就调用waitGroup.wait()方法
    3. 计数还没归零, 就再次调用 waitGroup.Add()方法

waitgroup是否可以重用?
可以重用. 但是必须等待计数归零.
简单来说就是在waitgroup归零之前, 只能有一次Add()动作

  1. Cond条件变量的实现机制?
    cond的作用?
    Cond目的是,等待 / 通知场景下的并发问题提供支持。
    Cond 常应用于等待某个条件的一组 goroutine,
    等条件变为 true 的时候,
    其中一个 goroutine 或者 所有的 goroutine 都会被唤醒执行。

顾名思义,Cond 是和某个条件相关,这个条件需要一组 goroutine 协作共同完成,
在条件还没有满足的时候,所有等待这个条件的 goroutine 都会被阻塞住,
只有协作达到了这个条件,等待的 goroutine 才可能继续进行下去。

原理(互斥锁+队列):
1.Signal 方法(唤醒一个),
允许调用者 Caller 唤醒一个等待此 Cond 的 goroutine。
如果此时没有等待的 goroutine,无需通知 waiter;
如果 Cond 等待队列中有一个或者多个等待的 goroutine,则需要从等待队列中移除第一个 goroutine 并把它唤醒。

在其他编程语言中,比如 Java 语言中,Signal 方法也被叫做 notify 方法。 
调用 Signal 方法时,不强求你一定要持有 c.L 的锁。

2.Broadcast 方法(唤醒所有协程),
允许调用者 Caller 唤醒所有等待此 Cond 的 goroutine。
如果此时没有等待的 goroutine,无需通知 waiter;
如果 Cond 等待队列中有一个或者多个等待的 goroutine,则清空所有等待的 goroutine,并全部唤醒。

在其他编程语言中,比如 Java 语言中,Broadcast 方法也被叫做 notifyAll 方法。 	
同样地,调用 Broadcast 方法时,也不强求你一定持有 c.L 的锁。

3.Wait方法(放入队列阻塞):

  1. Cond常见出错情况:

    1. 调用 wait时没有加锁
    2. 只调用一次 wait, 没有检查等待条件是否满足, 结果条件没满足, 就向下执行了
  2. Once的概念及原理?
    sync.Once :
    sync.Once 只暴露了一个方法 Do,
    你可以多次调用 Do 方法,但是只有第一次调用 Do 方法时 f 参数才会执行,
    这里的 f 是一个无参数无返回值的函数。

func main() {
    var once sync.Once
    // 第一个初始化函数
    f1 := func() {
       fmt.Println("in f1")
    }
    once.Do(f1) // 打印出 in f1
    // 第二个初始化函数
    f2 := func() {
       fmt.Println("in f2")
    }
    once.Do(f2) // 无输出
}

  1. sync.Once并发原语
    Once 可以用来执行且仅仅执行一次动作,常常用于单例对象的初始化场景。

func main() {
    var once sync.Once
    // 第一个初始化函数
    f1 := func() {
       fmt.Println("in f1")
    }
    once.Do(f1) // 打印出 in f1
    // 第二个初始化函数
    f2 := func() {
       fmt.Println("in f2")
    }
    once.Do(f2) // 无输出
}
  1. Once使用常见错误
    1. 死锁
func main() {
    var once sync.Once
    once.Do(func() {
       once.Do(func() {
          fmt.Println("初始化")
       })
    })
}
2. 未初始化
	如果第一次初始化方法执行了, 但是连接等资源没有初始化成功, 其它地方又引用了没初始化成功的资源, 导致panic
	解决方法:
	自己实现Once
// 一个功能更加强大的Once
type Once struct {
    m    sync.Mutex
    done uint32
}

// 传入的函数f有返回值error,如果初始化失败,需要返回失败的error
// Do方法会把这个error返回给调用者
func (o *Once) Do(f func() error) error {
    if atomic.LoadUint32(&o.done) == 1 { //fast path
       return nil
    }
    return o.slowDo(f)
}

// 如果还没有初始化
func (o *Once) slowDo(f func() error) error {
    o.m.Lock()
    defer o.m.Unlock()
    var err error
    if o.done == 0 { // 双检查,还没有初始化
       err = f()

       if err == nil { // 初始化成功才将标记置为已初始化
          atomic.StoreUint32(&o.done, 1)
       }
    }
    return err
}

  1. 普通map使用注意事项:
    1.key 类型的 K 必须是可比较的
    2.如果获取一个不存在的 key 对应的值时,会返回零值。
    为了区分真正的零值和 key 不存在这两种情况,可以根据第二个返回值来区分
    3.map 是无序的,

  2. map使用常见问题?
    1.使用前必须初始化
    2.原生map不支持并发读写

  3. 如何实现线程安全的 map类型?
    1. 方式1: 加读写锁
    2. 方式2: 分片加锁
    “github.com/orcaman/concurrent-map” 分片锁的map
    3. 方式3: 使用sync.Map
    sync.Map原理:
    原理
    sync.Map底层使用了两个原生map,一个叫read,仅用于读;一个叫dirty,用于在特定情况下存储最新写入的key-value数据:
    在这里插入图片描述sync.Map 的实现原理可概括为:
    通过 read 和 dirty 两个字段实现数据的读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上
    读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty
    读取 read 并不需要加锁,而读或写 dirty 则需要加锁
    另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据更新到 read 中(触发条件:misses=len(dirty))
    在这里插入图片描述
    优缺点
    优点:Go官方所出;通过读写分离,降低锁时间来提高效率;
    缺点:不适用于大量写的场景,这样会导致 read map 读不到数据而进一步加锁读取,同时dirty map也会一直晋升为read map,整体性能较差,甚至没有单纯的 map+metux 高。
    适用场景:读多写少的场景。

  4. Pool的概念?
    创建对象后, 想要重复利用对象, 不被GC回收
    创建pool存储起来, 实现重复利用

  5. sync.Pool的使用?
    有两个知识点你需要记住:

  6. sync.Pool 本身就是线程安全的,多个 goroutine 可以并发地调用它的方法存取对象;

  7. sync.Pool 不可在使用之后再复制使用。

  8. pool相关第三方库?
    bytebufferpool
    oxtoacart/bpool

  9. sync.pool存取元素的流程?
    pool中有P数量对应的local对象,
    local对象有private 和 shared字段
    private 只能由本地P存取, 所以没有并发问题
    shared 可以由任意P访问, 但只有本地P才能存入, 其它P只能消费

存取流程:
1.尝试从本地share中获取元素
2.如果为空, 从其它P的share队列尾部获取
3.如果还是获取不到, 调用New()方法创建
  1. context概念?
    上下文(Context)用来传递的除了业务参数之外的额外信息。
    Go 标准库中的 Context 功能还不止于此,它还提供了超时(Timeout)和取消(Cancel)的机制
4个基本方法?
Deadline()方法:
Done()方法:
Value()方法:
Err()方法:
  1. 原子操作?
    Package sync/atomic 实现了同步算法底层的原子的内存操作原语,我们把它叫做原子操作原语,
    它提供了一些实现原子操作的方法。

add()方法: 给目标值加参数值
CAS()方法:比较期待值, 如果相等, 再和参数值进行更新
Swap()方法:不需要比较, 直接和参数值进行更新
Load()方法: 原子加载目标值
Store()方法: 原子保存目标值

  1. channel的应用场景:
    1.数据交流
    2.数据传递
    3.信号通知
    4.任务编排
    5.锁

  2. channel的实现原理
    channel的类型是 runtime.hchan
    1.qcount属性: 计算还没被取走的元素个数
    2.dataqsize属性: 队列大小
    3.buf属性: 存放元素的 buffer
    4.sendx: 发送数据的指针在 buf中的位置
    5.recvx: 接收指针的 buf中的位置
    6.recvq: 接收者等待队列
    7.sendq: 发送者等待队列

  3. channel常见的错误:
    1.close没初始化的channel
    2.send已经close的channel
    3.close已经close的channel

  4. channel产生的 goroutine内存泄漏问题?
    接收者或者发送者的 goroutine, 一直等不到自己需要的管道信号, 导致大量的协程阻塞.

  5. f

  6. f

  7. f

  8. f

  9. f

  10. f

  11. f

  12. f

  13. f

  14. f

  15. f

  16. f

  17. f

  18. f

  19. f

  20. f

  21. f

  22. f

  23. f

  24. f

  25. f

  26. f

  27. f

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值