原子操作指的是在多线程或并发环境中,某个操作要么完全执行,要么完全不执行,不会被中途打断或者干扰。原子操作是保证数据一致性和防止数据竞争的关键技术
- 不可分割:一个原子操作在执行时是一个不可分割的单位,其他线程无法在它执行期间插入任何操作。
- 线程安全:多个线程同时对某一资源执行原子操作时,不会出现数据不一致或冲突。
- 高效性:原子操作通常由硬件级别(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) }
程序的工作原理:
- 定义了一个共享变量 counter 和一个互斥锁 mutex。
- 两个 goroutine 会同时尝试修改 counter。
- 在每次修改 counter 时,先调用 mutex.Lock() 加锁,确保其他 goroutine 无法访问 counter。
- 修改完成后,调用 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) |
---|---|---|
开销 | 低(硬件支持) | 较高(上下文切换) |
适用场景 | 简单变量操作 | 复杂逻辑或多变量 |
代码复杂度 | 简单 | 需要显式加锁/解锁 |
灵活性 | 有限 | 高 |