【Golang 面试 - 进阶题】每日 3 题(四)

✍个人博客:Pandaconda-CSDN博客

📣专栏地址:http://t.csdnimg.cn/UWz06

📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~

❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪

16. 什 么是 sync.Once?

sync.Once 是 Go 语言中的一个同步原语,用于实现只执行一次的操作。它可以保证在多个 Goroutine 中只执行一次指定的操作,即使这个操作被多次调用。

sync.Once 的使用非常简单,只需要创建一个 sync.Once 类型的变量,然后使用 Do() 方法来指定要执行的操作。Do() 方法会保证指定的操作只会被执行一次,无论它被调用多少次。下面是一个简单的例子:

Go 复制代码
var once sync.Once
func setup() {
    fmt.Println("Performing setup...")
}
func main() {
    // 在第一次调用时执行 setup 函数
    once.Do(setup)
    // 在第二次调用时不执行任何操作
    once.Do(func() { fmt.Println("This shouldn't be printed.") })
}

在这个例子中,我们首先定义了一个 sync.Once 类型的变量 once。然后,我们使用 once.Do() 方法来指定要执行的操作。在第一次调用时,Do() 方法会执行 setup() 函数,输出 "Performing setup..."。在第二次调用时,Do() 方法不会执行任何操作,因为 setup() 函数已经被执行过了。

需要注意的是,Do() 方法是阻塞的,也就是说,在第一次调用还没有完成之前,后续的调用会被阻塞。这个特性可以用来保证只有一个 Goroutine 执行指定的操作,而其他 Goroutine 等待它完成之后再继续执行。此外,Do() 方法只会执行一次指定的操作,即使在多个 Goroutine 中调用它。这个特性可以用来避免重复初始化等问题。

17. 什么 操作叫做原子操作?

在并发编程中,原子操作是一种不可中断的操作,要么全部完成,要么全部不完成。这意味着在多线程环境下,原子操作可以保证数据的一致性和可靠性,防止多个线程同时对同一数据进行操作而导致的竞争条件和数据不一致。

在 Go 语言中,sync/atomic 包提供了一些原子操作函数,用于在多线程环境中执行原子操作。这些原子操作函数可以确保对共享变量的访问是原子的,即不会被其他线程打断。

例如,atomic.AddInt64() 函数可以对一个 int64 类型的变量进行原子加法操作。以下是一个简单的示例:

Go 复制代码
package main
import (
    "fmt"
    "sync/atomic"
)
func main() {
    var count int64
    // 对 count 变量进行 100 次原子加法操作
    for i := 0; i < 100; i++ {
        atomic.AddInt64(&count, 1)
    }
    fmt.Println("count:", count)
}

在这个例子中,我们首先定义了一个 int64 类型的变量 count,并使用 atomic.AddInt64() 函数对它进行 100 次原子加法操作。AddInt64() 函数的第一个参数是一个指向 int64 类型变量的指针,它告诉函数要对哪个变量进行原子操作。第二个参数是要添加的值。在这个例子中,我们每次添加的值都是 1。最后,我们输出 count 变量的值,应该是 100。

需要注意的是,原子操作函数仅保证对共享变量的访问是原子的,但并不能保证对多个变量之间的操作是原子的。如果需要对多个变量进行原子操作,可以使用互斥锁或其他同步机制来保证线程安全。

18. Go 原子操作有哪些?

Go atomic 包是最轻量级的锁(也称无锁结构),可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作,不过这个包只支持 int32/int64/uint32/uint64/uintptr 这几种数据类型的一些基础操作(增减、交换、载入、存储等)。

概念:

原子操作仅会由一个独立的 CPU 指令代表和完成。原子操作是无锁的,常常直接通过 CPU 指令直接实现。 事实上,其它同步技术的实现常常依赖于原子操作。

使用场景:

当我们想要对某个变量 并发安全的修改,除了使用官方提供的 mutex,还可以使用 sync/atomic 包的原子操作,它能够保证对变量的读取或修改期间不被其他的协程所影响。

atomic 包提供的原子操作能够确保任一时刻只有一个 goroutine 对变量进行操作,善用 atomic 能够避免程序中出现大量的锁操作。

常见操作:

  • 增减 Add

  • 载入 Load

  • 比较并交换 CompareAndSwap

  • 交换 Swap

  • 存储 Store

atomic 操作的对象是一个地址,你需要把可寻址的变量的地址作为参数传递给方法,而不是把变量的值传递给方法。下面将分别介绍这些操作:

增减操作

此类操作的前缀为 Add

Go 复制代码
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

需要注意的是,第一个参数必须是指针类型的值,通过指针变量可以获取被操作数在内存中的地址,从而施加特殊的 CPU 指令,确保同一时间只有一个 goroutine 能够进行操作。

使用举例:

Go 复制代码
func add(addr *int64, delta int64) {
    atomic.AddInt64(addr, delta) //加操作
    fmt.Println("add opts: ", *addr)
}

载入操作

此类操作的前缀为 Load

Go 复制代码
func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
// 特殊类型: Value类型,常用于配置变更
func (v *Value) Load() (x interface{}) {}

载入操作能够保证原子的读变量的值,当读取的时候,任何其他 CPU 操作都无法对该变量进行读写,其实现机制受到底层硬件的支持。

使用示例:

Go 复制代码
func load(addr *int64) {
    fmt.Println("load opts: ", atomic.LoadInt64(&opts))
}

比较并交换

此类操作的前缀为 CompareAndSwap,该操作简称 CAS,可以用来实现乐观锁:

Go 复制代码
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)

该操作在进行交换前首先确保变量的值未被更改,即仍然保持参数 old 所记录的值,满足此前提下才进行交换操作。CAS 的做法类似操作数据库时常见的乐观锁机制。

需要注意的是,当有大量的 goroutine 对变量进行读写操作时,可能导致 CAS 操作无法成功,这时可以利用 for 循环多次尝试。

使用示例:

Go 复制代码
func compareAndSwap(addr *int64, oldValue int64, newValue int64) {
    if atomic.CompareAndSwapInt64(addr, oldValue, newValue) {
        fmt.Println("cas opts: ", *addr)
        return
    }
}

交换

此类操作的前缀为 Swap

Go 复制代码
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)

相对于 CAS,明显此类操作更为暴力直接,并不管变量的旧值是否被改变,直接赋予新值然后返回被替换的值。

Go 复制代码
func swap(addr *int64, newValue int64) {
    atomic.SwapInt64(addr, newValue)
    fmt.Println("swap opts: ", *addr)
}

存储

此类操作的前缀为 Store

Go 复制代码
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
// 特殊类型: Value类型,常用于配置变更
func (v *Value) Store(x interface{})

此类操作确保了写变量的原子性,避免其他操作读到了修改变量过程中的脏数据。

Go 复制代码
func store(addr *int64, newValue int64) {
    atomic.StoreInt64(addr, newValue)
    fmt.Println("store opts: ", *addr)
}
相关推荐
喵叔哟7 分钟前
重构代码之用委托替代继承
开发语言·重构
lzb_kkk13 分钟前
【JavaEE】JUC的常见类
java·开发语言·java-ee
SEEONTIME13 分钟前
python-24-一篇文章彻底掌握Python HTTP库Requests
开发语言·python·http·http库requests
起名字真南32 分钟前
【OJ题解】C++实现字符串大数相乘:无BigInteger库的字符串乘积解决方案
开发语言·c++·leetcode
tyler_download43 分钟前
golang 实现比特币内核:实现基于椭圆曲线的数字签名和验证
开发语言·数据库·golang
小小小~44 分钟前
qt5将程序打包并使用
开发语言·qt
hlsd#44 分钟前
go mod 依赖管理
开发语言·后端·golang
小春学渗透1 小时前
Day107:代码审计-PHP模型开发篇&MVC层&RCE执行&文件对比法&1day分析&0day验证
开发语言·安全·web安全·php·mvc
杜杜的man1 小时前
【go从零单排】迭代器(Iterators)
开发语言·算法·golang
陈大爷(有低保)1 小时前
三层架构和MVC以及它们的融合
后端·mvc