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

✍个人博客:Pandaconda-CSDN博客

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

📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪

19. 原 子操作和锁的区别

原子操作是一种特殊的操作,它可以在单个 CPU 指令周期内完成对共享变量的读取和修改,从而保证了操作的原子性。在 Go 中,使用 sync/atomic 包提供的原子操作函数可以对共享变量进行原子操作,从而避免了多个 Goroutine 对同一变量进行并发修改时出现的竞争条件问题。原子操作不需要获取锁,因此效率比锁更高,但是只适用于一些简单的操作,比如读取和修改整数类型的变量。

锁是另一种保证并发安全的机制,它可以确保同一时间只有一个 Goroutine 可以访问共享资源。在 Go 中,使用 sync 包提供的锁可以实现互斥访问共享资源的目的。锁的机制需要获取锁才能对共享变量进行操作,因此效率比原子操作略低。但是锁可以用于任何类型的变量,而不仅仅是整数类型的变量,因此可以用于更复杂的操作。

Go 中的原子操作和锁(Mutex)都是用于实现并发控制的机制,它们的主要区别如下:

1、作用范围

原子操作用于对单个共享变量的读写进行原子性的保障,而锁则用于对一段代码(即临界区)进行互斥访问,保障多个 goroutine 之间对共享资源的访问顺序。

2、并发性

原子操作的实现是通过硬件级别上的指令保证原子性,因此在高并发情况下执行效率较高。而锁则需要在多个 goroutine 之间进行状态切换和内核态和用户态之间的切换,因此在高并发情况下执行效率相对较低。

3、使用场景

原子操作适合于对单个共享变量进行频繁的读写操作,例如计数器等场景。锁则适用于需要对一段临界区进行互斥访问的场景,例如多个 goroutine 对同一数据结构进行操作时。

4、错误处理

在使用原子操作时,出现错误可能会导致程序崩溃,因此需要仔细地处理错误。而在使用锁时,错误处理相对简单,可以使用 defer 关键字保证锁的正确释放。

综上所述,如果需要对简单的整数类型变量进行原子操作,可以使用原子操作;如果需要对任意类型的变量进行并发安全的操作,应该使用锁。需要根据具体的应用场景选择使用哪种机制,以获得最佳的性能和可靠性。

20. 什么是 CAS?

CAS,即 Compare-And-Swap,是一种常见的并发控制机制,也是原子操作的一种。它用于实现在多个线程并发修改同一数据时的同步和互斥访问,是实现锁、并发队列等数据结构的基础。

CAS 操作需要三个参数:内存地址 V,期望值 A 和新值 B。CAS 操作的执行过程如下:

  1. 比较内存地址 V 中存储的值与期望值 A 是否相等;

  2. 如果相等,则将内存地址 V 中存储的值更新为新值 B;

  3. 如果不相等,则说明其他线程已经修改了内存地址 V 中存储的值,此时 CAS 操作失败,需要重新尝试。

在 Go 中,使用 sync/atomic 包提供的 CompareAndSwapXXX() 函数可以执行 CAS 操作,其中 XXX 表示不同的数据类型。例如,CompareAndSwapInt32() 函数用于对一个 int32 类型的变量执行 CAS 操作。以下是一个简单的示例:

package main
import (
    "fmt"
    "sync/atomic"
)
func main() {
    var value int32 = 1
    // 将 value 变量的值从 1 修改为 2
    atomic.CompareAndSwapInt32(&value, 1, 2)
    fmt.Println("value:", value)
}

在这个例子中,我们首先定义了一个 int32 类型的变量 value,并使用 CompareAndSwapInt32() 函数将其从 1 修改为 2。CompareAndSwapInt32() 函数的第一个参数是一个指向 int32 类型变量的指针,它告诉函数要对哪个变量进行 CAS 操作。第二个参数是期望值 A,第三个参数是新值 B。如果 value 的值与期望值 A 相等,则函数会将 value 的值更新为新值 B,并返回 true,否则不会更新 value 的值,并返回 false。在这个例子中,value 的初始值是 1,期望值 A 是 1,新值 B 是 2,因此 CAS 操作会成功,value 的值会被更新为 2。

需要注意的是,CAS 操作虽然可以避免锁的使用,提高了并发性能,但是也存在一些问题,比如 ABA 问题。因此在使用 CAS 操作时,需要谨慎设计并发控制策略,以确保线程安全。

21. syn c.Pool 有什么用?

sync.Pool 是 Go 标准库中的一个对象池实现,它的作用是缓存对象,减少对象的创建和垃圾回收,从而提高程序的性能。

在程序中,创建和销毁对象是很耗费时间和资源的操作,特别是在高并发情况下。如果能够复用已经创建好的对象,就可以减少对象的创建和垃圾回收,提高程序的性能。这就是对象池的作用。

sync.Pool 的实现比较简单,它维护了两个对象池:一个是空闲对象池,用于存储可重复使用的对象;另一个是新对象池,用于存储不能重复使用的对象。在使用对象时,首先从空闲对象池中获取对象,如果空闲对象池为空,则从新对象池中获取对象,如果新对象池也为空,则创建一个新对象。使用完对象后,将对象放回空闲对象池中。

需要注意的是,sync.Pool 并不保证对象一定会被重用。如果空闲对象池中没有可用的对象,或者对象已经达到了一定的数量限制,那么 sync.Pool 会选择创建新对象。因此,在使用 sync.Pool 时,需要谨慎设计对象的数量和生命周期,以确保对象的重复使用。

以下是一个简单的示例,演示了如何使用 sync.Pool

package main
import (
    "fmt"
    "sync"
)
type Object struct {
    // 定义对象属性
}
func main() {
    var pool = sync.Pool{
        New: func() interface{} {
            // 创建新对象
            return new(Object)
        },
    }
    // 获取对象
    object1 := pool.Get().(*Object)
    fmt.Printf("object1: %p\n", object1)
    // 放回对象池中
    pool.Put(object1)
    // 再次获取对象
    object2 := pool.Get().(*Object)
    fmt.Printf("object2: %p\n", object2)
}

在这个示例中,我们首先创建了一个对象池 pool,并指定了 New 函数,用于创建新对象。然后我们从对象池中获取一个对象 object1,并打印它的内存地址。接着,我们将对象 object1 放回对象池中,然后再次从对象池中获取对象 object2,并打印它的内存地址。由于对象 object1 已经放回对象池中,所以对象池会优先从空闲对象池中获取对象,因此对象 object2 的内存地址和对象 object1 的内存地址相同。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值