返回顶部
首页 > 资讯 > 后端开发 > GO >Go语言atomic.Value如何不加锁保证数据线程安全?
  • 840
分享到

Go语言atomic.Value如何不加锁保证数据线程安全?

Go语言atomic.ValueGo数据线程安全 2023-05-20 05:05:42 840人浏览 安东尼
摘要

目录引言atomic.Value的使用方式atomic.Value的内部实现写入线程安全的保证读取(Load)操作总结引言 很多人可能没有注意过,在 Go(甚至是大部分语言)中,一条

引言

很多人可能没有注意过,在 Go(甚至是大部分语言)中,一条普通的赋值语句其实不是一个原子操作。例如,在32位机器上写int64类型的变量就会有中间状态,它会被拆成两次写操作(汇编的MOV指令)——写低 32 位和写高 32 位。32机器上对int64进行赋值

如果一个线程刚写完低32位,还没来得及写高32位时,另一个线程读取了这个变量,那它得到的就是一个毫无逻辑的中间变量,这很有可能使我们的程序出现Bug。

这还只是一个基础类型,如果我们对一个结构体进行赋值,那它出现并发问题的概率就更高了。很可能写线程刚写完一小半的字段,读线程就来读取这个变量,那么就只能读到仅修改了一部分的值。这显然破坏了变量的完整性,读出来的值也是完全错误的。

面对这种多线程下变量的读写问题,Go给出的解决方案是atomic.Value,它使得我们可以不依赖于不保证兼容性的unsafe.Pointer类型,同时又能将任意数据类型的读写操作封装成原子性操作。

atomic.Value的使用方式

atomic.Value类型对外提供了两个读写方法:

  • v.Store(c) - 写操作,将原始的变量c存放到一个atomic.Value类型的v里。
  • c := v.Load() - 读操作,从内存中线程安全的v中读取上一步存放的内容。

下面是一个简单的例子演示atomic.Value的用法。

type Rectangle struct {
	length int
	width  int
}
var rect atomic.Value
func update(width, length int) {
	rectLocal := new(Rectangle)
	rectLocal.width = width
	rectLocal.length = length
	rect.Store(rectLocal)
}
func main() {
	wg := sync.WaitGroup{}
	wg.Add(10)
	// 10 个协程并发更新
	for i := 0; i < 10; i++ {
		go func(i int) {
			defer wg.Done()
			update(i, i+5)
		}(i)
	}
	wg.Wait()
	r := rect.Load().(*Rectangle)
	fmt.Printf("rect.width=%d\nrect.length=%d\n", r.width, r.length)
}

你可能会好奇,为什么atomic.Value在不加的情况下就提供了读写变量的线程安全保证,接下来我们就一起看看其内部实现。

atomic.Value的内部实现

atomic.Value被设计用来存储任意类型的数据,所以它内部的字段是一个interface{}类型。

// A Value provides an atomic load and store of a consistently typed value.
// The zero value for a Value returns nil from Load.
// Once Store has been called, a Value must not be copied.
//
// A Value must not be copied after first use.
type Value struct {
	v interface{}
}

除了Value外,atomic包内部定义了一个ifaceWords类型,这其实是interface{}的内部表示 (runtime.eface),它的作用是将interface{}类型分解,得到其原始类型(typ)和真正的值(data)。

// ifaceWords is interface{} internal representation.
type ifaceWords struct {
	typ  unsafe.Pointer
	data unsafe.Pointer
}

写入线程安全的保证

直接来看代码

// Store sets the value of the Value to x.
// All calls to Store for a given Value must use values of the same concrete type.
// Store of an inconsistent type panics, as does Store(nil).
func (v *Value) Store(val interface{}) {
	if val == nil {
		panic("sync/atomic: store of nil value into Value")
	}
    // 通过unsafe.Pointer将现有的(v)和要写入的值(val) 分别转成ifaceWords类型。
    // 这样我们下一步就可以得到这两个interface{}的原始类型(typ)和真正的值(data)。
	vp := (*ifaceWords)(unsafe.Pointer(v))
	vlp := (*ifaceWords)(unsafe.Pointer(&val))
	for {
		typ := LoadPointer(&vp.typ)
		if typ == nil {
			// Attempt to start first store.
			// Disable preemption so that other goroutines can use
			// active spin wait to wait for completion; and so that
			// GC does not see the fake type accidentally.
			runtime_procPin()
			if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
				runtime_procUnpin()
				continue
			}
			// Complete first store.
			StorePointer(&vp.data, vlp.data)
			StorePointer(&vp.typ, vlp.typ)
			runtime_procUnpin()
			return
		}
		if uintptr(typ) == ^uintptr(0) {
			// First store in progress. Wait.
			// Since we disable preemption around the first store,
			// we can wait with active spinning.
			continue
		}
		// First store completed. Check type and overwrite data.
		if typ != vlp.typ {
			panic("sync/atomic: store of inconsistently typed value into Value")
		}
		StorePointer(&vp.data, vlp.data)
		return
	}
}

大概的逻辑:

  • 开始就是一个无限 for 循环。配合CompareAndSwap使用,可以达到乐观锁的效果。
  • 通过LoadPointer这个原子操作拿到当前Value中存储的类型。下面根据这个类型的不同,分3种情况处理。
  • 第一次写入

    一个atomic.Value实例被初始化后,它的typdata字段会被设置为指针的零值 nil,所以先判断如果typ是否为nil,如果是那就证明这个Value实例还未被写入过数据。那之后就是一段初始写入的操作:

  • runtime_procPin()这是runtime中的一段函数,一方面它禁止了调度器对当前 goroutine 的抢占(preemption),使得它在执行当前逻辑的时候不被其他goroutine打断,以便可以尽快地完成工作。另一方面,在禁止抢占期间,GC 线程也无法被启用,这样可以防止 GC 线程看到一个莫名其妙的指向^uintptr(0)的类型(这是赋值过程中的中间状态)。

    1)使用CAS操作,先尝试将typ设置为^uintptr(0)这个中间状态。如果失败,则证明已经有别的线程抢先完成了赋值操作,那它就解除抢占锁,然后重新回到 for 循环第一步。

    2)如果设置成功,那证明当前线程抢到了这个"乐观锁”,它可以安全的把v设为传入的新值了。注意,这里是先写data字段,然后再写typ字段。因为我们是以typ字段的值作为写入完成与否的判断依据的

  • 第一次写入还未完成

    如果看到typ字段还是^uintptr(0)这个中间类型,证明刚刚的第一次写入还没有完成,所以它会继续循环,一直等到第一次写入完成。

  • 第一次写入已完成

    首先检查上一次写入的类型与这一次要写入的类型是否一致,如果不一致则抛出异常。反之,则直接把这一次要写入的值写入到data字段。

这个逻辑的主要思想就是,为了完成多个字段的原子性写入,我们可以抓住其中的一个字段,以它的状态来标志整个原子写入的状态。

读取(Load)操作

先上代码:

// Load returns the value set by the most recent Store.
// It returns nil if there has been no call to Store for this Value.
func (v *Value) Load() (val interface{}) {
	vp := (*ifaceWords)(unsafe.Pointer(v))
	typ := LoadPointer(&vp.typ)
	if typ == nil || uintptr(typ) == ^uintptr(0) {
		// First store not yet completed.
		return nil
	}
	data := LoadPointer(&vp.data)
	vlp := (*ifaceWords)(unsafe.Pointer(&val))
	vlp.typ = typ
	vlp.data = data
	return
}

读取相对就简单很多了,它有两个分支:

  • 如果当前的typ是 nil 或者^uintptr(0),那就证明第一次写入还没有开始,或者还没完成,那就直接返回 nil (不对外暴露中间状态)。
  • 否则,根据当前看到的typdata构造出一个新的interface{}返回出去。

总结

本文由浅入深的介绍了atomic.Value的使用姿势,以及内部实现。另外,原子操作由底层硬件支持,对于一个变量更新的保护,原子操作通常会更有效率,并且更能利用计算机多核的优势,如果要更新的是一个复合对象,则应当使用atomic.Value封装好的实现。

而我们做并发同步控制常用到的Mutex锁,则是由操作系统调度器实现,锁应当用来保护一段逻辑。

以上就是Go语言atomic.Value如何不加锁保证数据线程安全?的详细内容,更多关于Go语言atomic.Value如何不加锁保证数据线程安全?的资料请关注编程网其它相关文章!

您可能感兴趣的文档:

--结束END--

本文标题: Go语言atomic.Value如何不加锁保证数据线程安全?

本文链接: https://lsjlt.com/news/213919.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Go语言atomic.Value如何不加锁保证数据线程安全?
    目录引言atomic.Value的使用方式atomic.Value的内部实现写入线程安全的保证读取(Load)操作总结引言 很多人可能没有注意过,在 Go(甚至是大部分语言)中,一条...
    99+
    2023-05-20
    Go语言atomic.Value Go数据线程安全
  • Go语言如何保证云上应用的数据安全性?
    随着云计算越来越成为企业IT部署的主要方式,越来越多的应用程序被迁移到云上运行。然而,对于许多企业来说,一个重要的问题就是如何确保在云上运行的应用程序的数据安全性。在这个领域中,Go语言是一个备受瞩目的技术选择。本文将介绍Go语言是如何保证...
    99+
    2023-05-17
    Go语言 云上应用 数据安全性
  • 详解Go语言Sync.Pool为何不加锁也能够实现线程安全
    目录1. 简介2. GMP之间的绑定关系2.1 M和P的关系2.2 P和G的关系2.3 总结3.Sync.Pool与GMP模型3.1 sync.Pool性能问题3.2 基于GMP模型...
    99+
    2023-05-17
    Go Sync.Pool线程安全 Go Sync.Pool线程 Go Sync.Pool
  • 如何在Go语言中使用锁实现线程安全
    在Go语言中使用锁实现线程安全 随着并发编程的不断普及,保证数据在多个goroutine之间安全访问变得尤为重要。在Go语言中,可以使用锁来实现线程安全,确保共享资源在并发环境下的访问...
    99+
    2024-04-02
  • Go语言线程安全之互斥锁与读写锁
    目录一、互斥锁是什么?1.概念2.未加锁3.加锁之后二、读写锁【效率革命】1.为什么读写锁效率高2.使用方法三、sync.once1.sync.once产生背景2.sync.once...
    99+
    2024-04-02
  • ConcurrentHashMap是如何保证线程安全
    目录JDK 1.7 底层实现JDK 1.7 线程安全实现JDK 1.8 底层实现JDK 1.8 线程安全实现总结ConcurrentHashMap 是 HashMap 的多线程版本,...
    99+
    2024-04-02
  • 浅谈springboot如何保证多线程安全
    目录如何保证多线程安全1.springboot在多线程并发访问下是怎么做的2.controller在多线程下如何尽可能保证线程安全,如何取舍3.小结一下单例模式与线程安全问题踩的坑下...
    99+
    2024-04-02
  • 详解CopyOnWriteArrayList是如何保证线程安全
    目录一:前言二:成员变量分析三:源码分析1.空参构造2.传入一个Collection对象的构造方法3.传入一个数组的构造方法四:总结一:前言 在我们需要保证线程安全的时候,如果使用到...
    99+
    2024-04-02
  • 云存储如何保证数据安全
    云存储保证数据安全的方法:1、将云存储的数据进行加密存储,从而提高云存储数据安全性和隐私性;2、做好完整性审计策略,保证云存储数据的完整性;3、访问获取云存储数据时,需要做好密文访问控制,避免黑客轻易破解窃取数据信息。具体内容如下:加密存储...
    99+
    2024-04-02
  • 高并发如何保证数据安全
    高并发保证数据安全的方法:在java中我们可以使用 setnx 的原子性来实现分布式锁保证数据唯一性。尽量让HTML静态化。将图片与页面进行分离。使用缓存、镜像、负载均衡的方法。需要使用数据库集群或者库表散列。...
    99+
    2024-04-02
  • Go语言对象文件存储:如何保护你的数据安全?
    随着互联网的发展,数据安全问题越来越受到关注。在现代化的应用程序中,数据存储是一个必不可少的部分。因此,保护数据的安全性变得越来越重要。在本文中,我们将介绍如何使用Go语言的对象文件存储来保护你的数据安全。 什么是对象文件存储? 对象...
    99+
    2023-09-30
    对象 文件 存储
  • Go语言如何支持云上的数据安全和隐私保护?
    随着云计算技术的不断发展,越来越多的企业和个人开始将数据存储在云服务提供商的服务器上。然而,数据安全和隐私保护成为了云计算领域中的一个重要问题,而Go语言能够提供一些有效的解决方案。一、Go语言的加密库Go语言内置了许多加密库,包括AES、...
    99+
    2023-05-18
    Go语言 云计算安全 数据隐私保护
  • 云服务器如何保证数据安全
    云服务器保证数据安全的方法:使用SSH安全保护,在SHH配置文件中设置访问IP或域名白名单、禁止root登录、复杂SSH密码、禁止通过文件直接登录等。将SSH常用的22号端口关闭,更改为10000以上的随机端口,防止端口扫描。将数据定期备份...
    99+
    2024-04-02
  • 服务器租用如何保证数据安全
    服务器租用保证数据安全的方法:1、需要做好服务器数据库维护措施,避免数据丢失;2、需要定期更新服务器软件程序,避免黑客利用漏洞窃取数据;3、需要做好服务器数据备份操作,防止数据丢失无法恢复;4、需要给服务器建立一个完善的灾难恢复机制,从而保...
    99+
    2024-04-02
  • 云服务器如何保证数据安全性
    云服务器保证数据安全性的方法:1、搭建完善的数据隔离防护系统,提高云服务器数据安全性;2、定时对云服务器数据进行备份;3、对存储在云服务器上的数据进行加密;4、避免所有数据在云端处理,做一个云服务器副本进行分类;5、做好身份认证,对访问云服...
    99+
    2024-04-02
  • 分布式存储如何保证数据安全
    分布式存储系统通常使用多个存储节点来存储数据,因此在保证数据安全方面有以下几个关键因素:1. 数据冗余:分布式存储系统会将数据复制到...
    99+
    2023-09-20
    分布式存储
  • 代理IP应用如何保证数据安全
    代理IP应用如何保证数据安全,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。目前,随着数据集中化程度越来越高,数据量越来越大,传统的信息安全手段已不能满足大数据时...
    99+
    2023-06-25
  • 裸金属服务器如何保证数据安全
    裸金属服务器保证数据安全的方法:1、裸金属服务器具备物理机级的性能和隔离性,用户独占计算资源;2、带有本地磁盘的裸金属服务器,能够支持本地磁盘组RAID,保证数据安全;3、无本地磁盘的裸金属服务器,能够支持从云硬盘启动,利用云服务器备份进行...
    99+
    2024-04-02
  • ASP IDE路径并发:如何保证数据安全?
    在ASP IDE开发中,有时候会出现路径并发的问题,这可能会导致数据被多个请求同时读写,从而引发数据安全问题。本文将介绍ASP IDE路径并发的概念及其解决方法,帮助开发者更好地保证数据安全。 一、什么是路径并发? 路径并发是指多个请求同...
    99+
    2023-10-11
    ide path 并发
  • 云服务器如何保证数据安全性能
    数据备份:云服务器会自动将数据备份到多个位置,以防止数据丢失或损坏。备份可以是定期自动备份或实时备份,以保证数据的安全性和可用性。 数据恢复:如果出现数据丢失或损坏的情况,云服务器会自动尝试恢复数据。如果无法恢复,云服务器会提供快速恢复机...
    99+
    2023-10-27
    性能 服务器 数据
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作