go原子级内存操作实现

来自:网络
时间:2024-03-30
阅读:

原子级内存操作是在多线程并发执行时,能够确保某个内存操作是不可中断的操作。在计算机系统中,CPU执行指令是基本的原子操作,即一个指令的执行是不可被中断的。然而,在多线程并发的环境中,一个线程执行的指令可能被其他线程的操作所干扰,导致数据不一致或产生竞态条件。

原子操作保证了对共享数据的操作是不可分割的,即要么完全执行,要么完全不执行。在多线程环境中,原子操作通常用于解决并发访问共享资源时可能出现的竞态条件问题。

在编程中,原子操作通常使用特殊的CPU指令或者操作系统提供的原子操作函数来实现。在Go语言中,sync/atomic 包提供了一组原子操作的函数,例如 Add, CompareAndSwap, Load, Store 等,用于执行原子级别的内存操作。

以下是一个使用Go中的sync/atomic包实现的简单示例:

package main

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

func main() {
	var counter int64

	// 使用原子操作增加计数器的值
	atomic.AddInt64(&counter, 1)

	// 使用原子操作获取计数器的值
	value := atomic.LoadInt64(&counter)
	fmt.Println("Counter:", value)

	// 使用原子操作比较并交换计数器的值
	success := atomic.CompareAndSwapInt64(&counter, 1, 2)
	fmt.Println("Swap success:", success)

	// 在多线程环境中,原子操作确保对共享数据的操作是线程安全的
	go func() {
		atomic.AddInt64(&counter, 1)
	}()

	go func() {
		atomic.AddInt64(&counter, 1)
	}()

	time.Sleep(time.Millisecond) // 等待goroutine执行完毕

	// 最终的计数器值
	finalValue := atomic.LoadInt64(&counter)
	fmt.Println("Final Counter:", finalValue)
}

运行结果

Counter: 1
Swap success: true
Final Counter: 4

原子操作与互斥锁的区别

互斥锁是一种数据结构,使你可以执行一系列互斥操作。而原子操作是互斥的单个操作,这意味着没有其他线程可以打断它。那么就Go语言里atomic包里的原子操作和sync包提供的同步锁有什么不同呢?

首先atomic操作的优势是更轻量,比如CAS可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作。这可以大大的减少同步对程序性能的损耗。

原子操作也有劣势。还是以CAS操作为例,使用CAS操作的做法趋于乐观,总是假设被操作值未曾被改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换,那么在被操作值被频繁变更的情况下,CAS操作并不那么容易成功。而使用互斥锁的做法则趋于悲观,我们总假设会有并发的操作要修改被操作的值,并使用锁将相关操作放入临界区中加以保护。

所以总结下来原子操作与互斥锁的区别有:

  • 互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。
  • 原子操作是针对某个值的单个互斥操作。
  • 可以把互斥锁理解为悲观锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

atomic包提供了底层的原子性内存原语,这对于同步算法的实现很有用。这些函数一定要非常小心地使用,使用不当反而会增加系统资源的开销,对于应用层来说,最好使用通道或sync包中提供的功能来完成同步操作。

针对atomic包的观点在Google的邮件组里也有很多讨论,其中一个结论解释是:

应避免使用该包装。或者,阅读C ++ 11标准的“原子操作”一章;如果您了解如何在C ++中安全地使用这些操作,那么你才能有安全地使用Go的sync/atomic包的能力。
返回顶部
顶部