GO并发编程方面的一些常见面试问题

Posted 了 凡

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GO并发编程方面的一些常见面试问题相关的知识,希望对你有一定的参考价值。

博主介绍:

我是了 凡 微信公众号【了凡银河系】期待你的关注。未来大家一起加油啊~


前言

下面记录一些关于Go并发编程方面的一些常见的问题


文章目录

1. sync包中Mutex 文件的功能和用法。(Mutex的4种状态,正常模式和饥饿模式,自旋)

sync包中的Mutex是一种互斥锁,用于保护临界区资源的并发访问。Mutex内部维护着一个状态标记和一个等待队列。

Mutex的四种状态分别是:

  1. 未加锁状态(0值状态)。
  2. 已加锁状态(锁定状态)。
  3. 饥饿状态,表示等待锁的goroutine处于饥饿状态。
  4. 唤醒状态,表示锁定状态的goroutine正在等待通知唤醒。

Mutex的用法非常简单,只需要调用Lock()方法来获得锁,并在操作完成后调用Unlock()方法来释放锁。当Mutex处于锁定状态时,其他的goroutine将会被阻塞。

Mutex使用了自旋技术来减少阻塞和解除阻塞的开销。自旋指的是在获取锁失败后,不会立即挂起当前goroutine,而是会循环检测锁状态,直到获取到锁或者等待超时。当发生竞争时,自旋能够让goroutine不断尝试获取锁,而不是让它们进入睡眠状态,从而降低了线程切换的开销,提高了并发效率。

Mutex还实现了饥饿模式,用于防止某些goroutine长期无法获取到锁的情况。当某个goroutine等待锁的时间过长时,Mutex会将其转换为饥饿状态,并将其添加到等待队列的最前面,优先让其获取锁。

总结:Mutex提供了一种非常简单、高效的方式来保护共享资源,避免了竞争的发生,并通过自旋技术和饥饿模式来提高并发效率。

2. RWMutex 的功能实现和使用时的注意事项。

RWMutex(读写锁)是 golang 中提供的一种锁机制,它可以实现对共享资源的读写访问控制。与 Mutex(互斥锁)相比,RWMutex 允许多个 goroutine 同时读取共享资源,但只允许一个 goroutine 写入共享资源。

RWMutex 实现的主要思想是:使用一个互斥锁 mutex 来保护共享资源的读写操作,并使用一个计数器来记录当前正在读取共享资源的 goroutine 数量。计数器为 0 时表示当前没有 goroutine 读写共享资源,此时可以直接获取写锁;计数器不为 0 时表示当前有 goroutine 在读取共享资源,此时需要等待所有读操作完成后才能获取写锁。

使用 RWMutex 时需要注意以下事项:

  1. 读写锁不能递归调用,即同一个 goroutine 在持有读锁或写锁时,不能再次请求持有相同的锁。
  2. 写锁优先级高于读锁,即当有 goroutine 请求写锁时,所有的读锁和写锁都必须等待,直到写锁被释放为止。
  3. 写锁和读锁不可同时持有,即当有 goroutine 持有读锁时,不能再请求写锁。
  4. 读写锁不能由多个 goroutine 同时操作,需要使用互斥锁来保证同步。

3. Broadcast 和 single 的区别。

在 Go 语言中,sync 包提供了两种类型的条件变量:Cond 和 Cond.Broadcast() 和 Cond.Signal() 方法,分别用于广播和单个唤醒等待在条件变量上的 goroutine。

广播(Broadcast)是指唤醒所有正在等待条件变量的 goroutine,让它们重新去检查条件是否满足。调用 Broadcast() 方法后,所有等待在该条件变量上的 goroutine 都会被唤醒,但是只有一个 goroutine 能够获取到该条件变量对应的锁,其他 goroutine 仍然需要等待该锁被释放后才能执行后续操作。

单个唤醒(Signal)是指唤醒等待在条件变量上的某一个 goroutine,让它重新去检查条件是否满足。调用 Signal() 方法后,等待在该条件变量上的某一个 goroutine 会被唤醒,但是其他等待在该条件变量上的 goroutine 仍然需要等待,直到该唤醒的 goroutine 释放该条件变量对应的锁后才能执行后续操作。

一般来说,Broadcast 适用于多个 goroutine 需要等待同一条件变量的情况,例如生产者-消费者模型中的消费者需要等待缓冲区非空时才能取出数据;而 Single 唤醒适用于多个 goroutine 等待同一条件变量,但只需要唤醒其中的一个 goroutine 即可,例如生产者-消费者模型中的生产者需要等待缓冲区不满时才能放入数据,但只需要唤醒其中一个生产者即可。

4. waitGroup的用法和实现原理。

在 Go 语言中,WaitGroup(等待组)是一种协调多个 goroutine 同步执行的机制。它可以用来等待一组 goroutine 执行完毕,然后再执行下一步操作。

WaitGroup 主要包含三个方法:Add、Done 和 Wait。

  • Add 方法用来添加等待的 goroutine 数量。
  • Done 方法用来标识一个等待的 goroutine 已经完成了它的工作。
  • Wait 方法用来等待所有等待的 goroutine 完成,直到计数器归零。

WaitGroup 的实现原理很简单:它使用一个计数器来跟踪等待的 goroutine 数量。当一个 goroutine 调用 Add 方法时,计数器加 1;当一个 goroutine 完成它的工作并调用 Done 方法时,计数器减 1;当一个 goroutine 调用 Wait 方法时,如果计数器不为 0,该方法会阻塞,直到计数器为 0。

在 WaitGroup 的实现中,计数器的值是原子性操作,可以通过 sync/atomic 包中的原子操作函数来进行读取和更新。因此,WaitGroup 是线程安全的,可以被多个 goroutine 同时使用。

需要注意的是,在使用 WaitGroup 时应该注意以下几点:

  1. 在每个等待的 goroutine 开始执行之前,必须调用 Add 方法来将计数器加 1。
  2. 在每个等待的 goroutine 结束之前,必须调用 Done 方法来将计数器减 1。
  3. 调用 Wait 方法时会阻塞当前 goroutine,直到计数器归零。
  4. WaitGroup 不是可重用的,一旦计数器归零,就不能再使用它等待其他 goroutine。

总结:WaitGroup 是 Go 语言中一种非常实用的同步机制,可以很方便地实现多个 goroutine 的同步等待操作。

5. 原子操作是什么,怎么实现,和锁的区别,CAS是什么。

原子操作是指在多线程并发执行时,某个操作要么全部执行成功,要么全部执行失败,不会存在部分执行成功的情况。在现代计算机体系结构中,原子操作通常是通过硬件指令来实现的,保证了操作的原子性。

在 Go 语言中,原子操作主要通过 sync/atomic 包来实现。该包提供了一系列的函数,可以对基本类型进行原子性的读写操作。这些函数包括 Add、CompareAndSwap、Load、Store 等等。

相比于锁的机制,原子操作的实现更加轻量级,没有锁的开销,可以更快地完成操作。但是,原子操作的局限性也比较大,因为它只适用于对单个变量的操作,无法处理多个变量之间的复杂关系。

CAS(Compare And Swap)是一种原子操作,它是指比较当前内存中某个变量的值与预期值是否相等,如果相等则将该变量的值替换成新的值。CAS 操作通常用于实现锁和并发数据结构等。

与锁的区别在于,锁是一种保护共享资源的机制,它可以确保在同一时刻只有一个线程可以访问共享资源,从而避免了竞争条件。而原子操作则是一种操作数据的机制,它保证了数据的原子性,但是并没有提供对共享资源的保护。

总结:原子操作是一种轻量级的并发编程机制,通过硬件指令来保证操作的原子性,可以高效地实现对单个变量的操作。与锁相比,原子操作更轻量级,但是其适用范围相对较窄。CAS 是一种常用的原子操作,可以用于实现锁和并发数据结构等。

6. Sync包的功能和用法。主要是Once和Pool的实现原理和具体用法。

Sync 包是 Go 语言标准库中提供的一个同步工具包,其中包含了多种同步原语,如 Mutex、Cond、Once、WaitGroup 等,可以实现不同场景下的同步与互斥。

其中,Once 和 Pool 是 Sync 包中比较常用的两个工具。

Once

Once 用于实现一次性的初始化操作,保证某个操作只会执行一次。它有一个 Do 方法,该方法接收一个函数作为参数,该函数只会被执行一次,无论 Do 方法被调用多少次。具体实现原理是利用了 sync.Once 结构体内部的 done 标志和互斥锁实现的。
下面是 Once 的使用示例:

package main

import (
    "fmt"
    "sync"
)

func main() 
    var once sync.Once
    var count int

    increment := func() 
        count++
    

    // 多次调用 Do 方法,但是函数只会被执行一次
    for i := 0; i < 10; i++ 
        once.Do(increment)
    

    fmt.Printf("Count is %d\\n", count) // Count is 1

Pool

Pool 用于提供对象的缓存和复用功能,能够减少内存分配的开销,提升程序的性能。它有 Get 和 Put 两个方法,Get 方法用于获取对象,如果 Pool 中没有可用对象,则会调用 New 函数创建一个新的对象,Put 方法用于归还对象到 Pool 中,以便下次使用。

下面是 Pool 的使用示例:

package main

import (
    "bytes"
    "fmt"
    "sync"
)

func main() 
    var pool sync.Pool

    // New 函数用于创建一个新的对象
    pool.New = func() interface 
        return bytes.NewBuffer([]byte)
    

    // 从 Pool 中获取一个对象
    buffer := pool.Get().(*bytes.Buffer)

    // 将对象还回 Pool 中
    defer pool.Put(buffer)

    // 使用对象
    buffer.WriteString("Hello, ")
    buffer.WriteString("world!")

    // 输出结果
    fmt.Println(buffer.String()) // Hello, world!

需要注意的是,Pool 并不是一个线程安全的类型,因此在并发环境中使用时,需要使用互斥锁进行保护。此外,由于 Pool 中的对象并没有被强制清空,因此可能会在对象被重用时,包含之前使用过的数据,需要在使用时进行初始化或清空操作。


需要系统学习的,推荐以下学习资源,需要的关注微信公众号发送序号领取下载:
000010:Go并发编程实践
000011:Go语言编译器简介


创作不易,点个赞吧!
如果需要后续再看点个收藏!
如果对我的文章有兴趣给个关注!
如果有问题,可以关注公众号【了凡银河系】点击联系我私聊。


以上是关于GO并发编程方面的一些常见面试问题的主要内容,如果未能解决你的问题,请参考以下文章

golang学习九:Go并发编程

GoLang并发控制(上)

Golang 并发模式

Golang 并发模式

Go语言学习之旅--并发编程

Go语言学习之旅--并发编程