logo小熊博客
首页 代码速查表 fk标记语言示例 登录
目录
go原子操作和通道以及互斥锁

原子操作指的是在多线程或并发环境中,某个操作要么完全执行,要么完全不执行,不会被中途打断或者干扰。原子操作是保证数据一致性和防止数据竞争的关键技术

  • 不可分割:一个原子操作在执行时是一个不可分割的单位,其他线程无法在它执行期间插入任何操作。
  • 线程安全:多个线程同时对某一资源执行原子操作时,不会出现数据不一致或冲突。
  • 高效性:原子操作通常由硬件级别(CPU)的指令直接支持,因此它们通常比加锁(如 mutex)更加高效。

原子操作的应用:

在 Go 中,原子操作主要通过 sync/atomic 包提供,用来处理基础类型(如 int32, int64 等)的并发访问。

常用的原子操作函数:

  • atomic.LoadInt32(addr *int32):原子读取一个 int32 值。
  • atomic.StoreInt32(addr *int32, val int32):原子写入一个 int32 值。
  • atomic.AddInt32(addr *int32, delta int32):原子地将 delta 加到 *addr 上,并返回新值。
  • atomic.CompareAndSwapInt32(addr *int32, old, new int32):如果 *addr == old,则将 *addr 替换为 new,返回是否成功。
  • atomic.SwapInt3(addr *int32, new int32):将 *addr 设置为 new,并返回旧值。
  • atomic.LoadPointer(addr *unsafe.Pointer):原子读取指针。
  • atomic.StorePointer(addr *unsafe.Pointer, val unsafe.Pointer):原子写入指针。
package main

import (
    "fmt"
    "sync/atomic"
)

var counter int32 // 使用原子操作更新这个变量

func main() {
    // 增加计数器
    atomic.AddInt32(&counter, 1)

    // 原子地获取计数器的值
    val := atomic.LoadInt32(&counter)

    fmt.Println(val) // 输出 1
}

在上面的示例中,atomic.AddInt32()atomic.LoadInt32() 都是原子操作,保证了在多线程环境下对 counter 的并发访问不会导致数据竞争。

通道(Channel)

通道是 Go 中用于不同 goroutine 之间传递数据的一种机制。通道不仅是数据的载体,还具有同步的功能,使得 goroutine 在并发执行时可以安全地交换信息。

通道的关键特点:
  • 类型安全:每个通道都有指定的类型,只有指定类型的数据可以通过该通道传输。
  • 同步:通道本质上是同步的,发送数据的 goroutine 会被阻塞,直到另一个 goroutine 接收到数据;接收数据的 goroutine 会被阻塞,直到有数据发送过来。这种同步特性帮助避免并发中的竞态条件。
  • 缓冲通道与非缓冲通道:通道有两种类型:
  • 非缓冲通道:发送操作会阻塞,直到有接收操作。
  • 缓冲通道:如果通道已满,发送操作会阻塞;如果通道为空,接收操作会阻塞。
通道的基本操作:
  • ch := make(chan Type):创建一个新的通道。
  • ch <- value:向通道发送数据。
  • value := <- ch:从通道接收数据。
  • close(ch):关闭通道,表示不再发送数据。
使用通道传递数据
package main

import "fmt"

func main() {
    ch := make(chan string) // 创建一个通道

    // 启动一个 goroutine 发送数据
    go func() {
        ch <- "Hello, Go!" // 发送数据到通道
    }()

    // 接收数据
    msg := <-ch
    fmt.Println(msg) // 输出 "Hello, Go!"
}

使用通道同步

通道不仅传递数据,还可以用作同步信号,通知其他 goroutine 完成某项任务。

package main

import "fmt"

func main() {
    done := make(chan bool) // 用于同步的通道

    go func() {
        // 执行一些任务
        fmt.Println("Task completed")
        done <- true // 任务完成,发送信号
    }()

    // 等待任务完成
    <-done
    fmt.Println("Main goroutine exits")
}

互斥锁(Mutex)

互斥锁(Mutex) 是一种用于多线程编程的同步机制,用于防止多个线程同时访问共享资源导致数据不一致的问题。互斥锁确保在某一时刻,只有一个线程能够访问受保护的资源。当一个线程获取互斥锁时,其他线程会被阻塞,直到锁被释放。

在 Go 中,可以使用 sync.Mutex 来实现互斥锁。主要有两个方法:

  • Lock():加锁,如果锁已被其他线程持有,调用线程会阻塞。
  • Unlock():解锁,释放锁。
Go 中互斥锁的使用示例

使用互斥锁保护共享变量

package main

import (
    "fmt"
    "sync"
    "time"
)

var (
    counter int        // 共享资源
    mutex   sync.Mutex // 互斥锁
)

func increment(wg *sync.WaitGroup) {
    defer wg.Done() // 确保 goroutine 完成时通知 WaitGroup

    for i := 0; i < 5; i++ {
        mutex.Lock()   // 获取锁
        counter++      // 修改共享变量
        fmt.Println("Counter:", counter)
        mutex.Unlock() // 释放锁
        time.Sleep(time.Millisecond * 100)
    }
}

func main() {
    var wg sync.WaitGroup
    wg.Add(2) // 设置需要等待的 goroutine 数量

    // 启动两个 goroutine
    go increment(&wg)
    go increment(&wg)

    wg.Wait() // 等待所有 goroutine 完成
    fmt.Println("Final Counter Value:", counter)
}

程序的工作原理:
  1. 定义了一个共享变量 counter 和一个互斥锁 mutex。
  2. 两个 goroutine 会同时尝试修改 counter。
  3. 在每次修改 counter 时,先调用 mutex.Lock() 加锁,确保其他 goroutine 无法访问 counter。
  4. 修改完成后,调用 mutex.Unlock() 解锁,允许其他 goroutine 继续。

读写互斥锁(RWMutex)

它允许多个goroutine同时读取共享资源,但同一时间只能有一个goroutine写入该资源。这种机制在读操作远多于写操作的场景下,能显著提升并发性能。

RWMutex的工作原理
  • 读锁: 多个goroutine可以同时获得读锁,这意味着多个goroutine可以同时读取共享资源。
  • 写锁: 任何时刻,只能有一个goroutine获得写锁,也就是说,当一个goroutine获得了写锁,其他所有试图获取读锁或写锁的goroutine都会被阻塞,直到该goroutine释放写锁。
  • 升级和降级: 当一个goroutine持有读锁时,如果它想要进行写操作,就需要先释放所有的读锁,然后尝试获取写锁。这个过程称为锁的升级。反之,当一个goroutine持有写锁时,如果它想要进行读操作,就需要先释放写锁,然后尝试获取读锁,这个过程称为锁的降级。
示例
package main

import (
        "fmt"
        "sync"
        "sync/atomic"
)

var (
        count int32
        rwmutex sync.RWMutex
)

func reader() {
        rwmutex.RLock()
        defer rwmutex.RUnlock()
        fmt.Println("read:", atomic.LoadInt32(&count))
}

func writer() {
        rwmutex.Lock()
        defer rwmutex.Unlock()
        atomic.AddInt32(&count, 1)
        fmt.Println("write:", count)
}

func main() {
        for i := 0; i < 10; i++ {
                go reader()
        }
        for i := 0; i < 5; i++ {
                go writer()
        }
        // 等待所有goroutine执行完成
}
RWMutex的注意事项
  • 锁的释放: 确保在完成对共享资源的访问后,及时释放锁。
  • 死锁: 避免在持有锁的情况下调用可能导致阻塞的函数,否则可能会导致死锁。
  • 性能开销: RWMutex虽然可以提高并发性能,但它也带来了一定的性能开销。在选择使用RWMutex时,需要权衡性能和并发性
RWMutex与互斥锁的区别
  • 互斥锁: 任何时刻只能有一个goroutine持有锁,读写操作都互斥。
  • 读写锁: 允许多个goroutine同时持有读锁,但写操作是互斥的。

与锁的对比

特性原子操作 (sync/atomic)互斥锁 (sync.Mutex)

开销

低(硬件支持)

较高(上下文切换)

适用场景

简单变量操作

复杂逻辑或多变量

代码复杂度

简单

需要显式加锁/解锁

灵活性

有限

上一篇:go类型转换和类型断言
下一篇:go获取当前时间
请我喝奶茶!
赞赏码
手机扫码访问
手机访问