深入了解Golang atomic原子操作

这篇具有很好参考价值的文章主要介绍了深入了解Golang atomic原子操作。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

并发安全性

   在编程中经常遇到并发而产生的问题,那么应该怎么解决并发呢?什么情况下会产生并发以及为什么会有并发问题的产生?下面我将从宏观的角度讲解并发安全性问题。

什么是并发?

   并发是指在同一时间间隔内,多个任务(线程、进程或活动)同时存在和执行的状态,在同一个时间点多个任务同时进行,当一个 CPU 在访问数据时,其他 CPU 可能同时在访问同一块数据或者相关的数据,如果其中一个 CPU 修改了数据并且写回了内存,其他 CPU 的缓存并不会立即更新,而是需要等待一定的时间或者触发特定的机制来使得缓存中的数据失效。这就可能导致其他 CPU 继续使用旧的数据,从而引发并发性问题,如数据不一致、程序错误等。

如何解决并发?

   在go中,可以使用GoroutineChannel来解决并发安全性,上者是基于共享通讯来解决并发安全性,亦或使用锁机制来解决并发安全,例如sync包下的atomicmutex。其实mutex就是基于atomic实现的,等到下章节将会讲解mutex的源码分析。锁呢又分为悲观锁和乐观锁,atomic是乐观锁而mutex是悲观锁

  • 悲观锁: 总是想到最坏的打算,只要它有发生意外的可能性,那么在访问共享资源之前立即获取锁,以确保同一时刻只有一个任务访问共享资源。悲观锁通常使用互斥锁和共享锁来实现,例如mutex,在Golang中除了atomic其他的都是悲观锁,悲观的优点就是简单易懂,能够有效地防止并发访问导致的数据不一致问题。缺点就是在高并发下会导致系统性能瓶颈,因为每次访问都需要获取锁,会造成线程阻塞和资源竞争。

  • 乐观锁: 乐观锁和悲观锁恰恰相反,它会认为并发访问不会导致数据冲突,所以在访问共享资源的时候不会立即获取锁,而是在更新操作的时候检查是否有其他事务对他进行了更改,乐观锁主要由CAS和版本号机制实现的,因为是无锁的,所以可以提高吞吐量。例如mysql中的MVCC机制就是通过乐观锁实现的,go中atomic也是乐观锁实现的一种方式

原子性的实现原理

   一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为原子性(atomicity) 。原子性意为不可再分割,即最小单位,要么都执行,要么都不执行。Golang的atomic包的原子性操作是通过CPU指令实现的,Golang的atomic包的原子操作函数会将变量的地址转换为指针型的变量,并使用CPU指令对这个指针型的变量进行操作,原子操作支持的类型包括int32、int64、uint32、uint64、uintptr、unsafe.Pointer

atomic

  了解了atomic是为了解决并发问题,官方并不建议使用atomic来保证并发安全性,除非在一些偏底层的操作中可以使用,一般使用mutex来保证并发安全性。下面开始正式讲解goalng下的atomic的使用和实现原理,他有5种原子性操作,
分别是

  • swap操作
  • compare-and-swap操作
  • add操作
  • load操作
  • store操作

上面这五种操作,其实内部都是用汇编语言进行操作的,所以这个并不是我们的重点,重点就在value

swap

  这个函数把addr 指针指向的内存里的值替换为新值new,然后返回旧值old,也就是把旧值修改成新值,然后把旧值返回出去。因为这里没有使用泛型,所以每个方法都要实现

func SwapInt32(addr *int32, new int32) (old int32)

func SwapInt64(addr *int64, new int64) (old int64)

func SwapUint32(addr *uint32, new uint32) (old uint32)

func SwapUint64(addr *uint64, new uint64) (old uint64)

func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)

func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

  这这并不是swap的具体实现,具体实现在汇编语言中即sync/atomic/asm.s,JMP指令被用来跳转到对应的Xchg函数,具体实现的方法在runtime∕internal∕atomic·Xchg中,需要注意的是每个cpu架构对应的指令也不一样,所以要先找到对应的架构文件,下面的例子都是基于x86实现的

TEXT ·SwapInt32(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Xchg(SB)

TEXT ·SwapUint32(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Xchg(SB)

TEXT ·SwapInt64(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Xchg64(SB)

TEXT ·SwapUint64(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Xchg64(SB)

TEXT ·SwapUintptr(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Xchguintptr(SB)

  在runtime∕internal∕atomic·Xchg中可以看到注释已经对这个汇编进行了解释,下面的例子都会基于int32类型进行讲解。Xchg 函数原子性地交换给定地址中的32位无符号整数的值,并返回原来的值,先把指针ptr指向的地址中的值读取并保存到临时变量中,在将参数 new 中的新值写入到指针 ptr 指向的地址中,即将旧值替换为新值,最后返回旧值

// uint32 Xchg(ptr *uint32, new uint32)
// Atomically:
//	old := *ptr;
//	*ptr = new;
//	return old;
TEXT ·Xchg(SB), NOSPLIT, $0-20
	MOVQ	ptr+0(FP), BX
	MOVL	new+8(FP), AX
	XCHGL	AX, 0(BX)
	MOVL	AX, ret+16(FP)
	RET

// uint64 Xchg64(ptr *uint64, new uint64)
// Atomically:
//	old := *ptr;
//	*ptr = new;
//	return old;

eg:下面是对swap使用的例子

var value int32

func main() {
	value = 100
	old := swapInt32(300)
	fmt.Printf("old:%d,value:%d", old, value)
}
func swapInt32(val int32) int32 {
	old := atomic.SwapInt32(&value, val)
	return old
}

输出结果:old:100,value:300

CompareAndSwap(CAS)

  CAS 操作会比较内存中某个位置的值与预期值,如果相等,则使用新值替换原值,并返回操作是否成功,在 Golang中,CAS 操作由sync/atomic包提供支持。atomic 包提供了一系列原子性操作函数,包括 CompareAndSwap、Add、Load、Store 等,用于对共享变量进行原子性操作。这些原子性操作函数可以在多线程环境下安全地对共享变量进行读写操作,而不需要显式地加锁


func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)

func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)

func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)

func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)

func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

JMP指令被用来跳转到对应的Cas函数

TEXT ·CompareAndSwapInt32(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Cas(SB)

TEXT ·CompareAndSwapUint32(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Cas(SB)

TEXT ·CompareAndSwapUintptr(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Casuintptr(SB)

TEXT ·CompareAndSwapInt64(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Cas64(SB)

TEXT ·CompareAndSwapUint64(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Cas64(SB)

  runtime/internal/atomic,这段汇编代码展示了Go语言中sync/atomic包中的Cas函数的底层实现。Cas函数用于实现原子比较并交换操作,其功能为:如果给定地址中的值等于旧值,则将新值写入到该地址中,并返回真(true);否则,返回假(false)。先将指针 ptr 指向的地址中的值加载到寄存器 AX 中,并将参数 old 中的旧值加载到寄存器 CX 中。在将参数 new 中的新值加载到寄存器 BX 中。然后使用 LOCK CMPXCHGL 指令进行比较并交换操作
这个过程保证了在多线程环境下对给定地址中的值进行原子性的比较和交换操作,从而避免了竞态条件和数据不一致的问题。

// bool Cas(int32 *val, int32 old, int32 new)
// Atomically:
//	if(*val == old){
//		*val = new;
//		return 1;
//	} else
//		return 0;
TEXT ·Cas(SB),NOSPLIT,$0-17
	MOVQ	ptr+0(FP), BX
	MOVL	old+8(FP), AX
	MOVL	new+12(FP), CX
	LOCK
	CMPXCHGL	CX, 0(BX)
	SETEQ	ret+16(FP)
	RET

eg:

var value int32
func main() {
	value = 100
	ok := compareAndSwapInt32(300)
	fmt.Printf("old:%v,value:%d", ok, value)
}
func compareAndSwapInt32(val int32) bool {
	ok := atomic.CompareAndSwapInt32(&value, val, val+101)
	return ok
}

只有value和val相等的时候才会替换value的值,这个和swap其实就多了一步判断
运行结果:old:false,value:100

Add(加或者减)

  从源码可以看出,Add只支持五种类型,不支持unsafe.Pointer类型,add就是把addr 指针指向的内存里的值和delta做加法,然后返回新值,

func AddInt32(addr *int32, delta int32) (new int32)

func AddUint32(addr *uint32, delta uint32) (new uint32)

func AddInt64(addr *int64, delta int64) (new int64)

func AddUint64(addr *uint64, delta uint64) (new uint64)

func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

  这些函数AddInt32AddUint32AddUintptrAddInt64AddUint64,它们分别用于对32位整数、无符号32位整数、指针类型、64位整数和无符号64位整数进行原子加法操作。
这些Add函数的实现都是通过调用名为runtime/internal/atomic包中的XaddXadduintptrXadd64函数来实现的。这些函数会原子性地将给定地址中的值加上一个指定的增量,并返回增加前的值。

TEXT ·AddInt32(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Xadd(SB)

TEXT ·AddUint32(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Xadd(SB)

TEXT ·AddUintptr(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Xadduintptr(SB)

TEXT ·AddInt64(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Xadd64(SB)

TEXT ·AddUint64(SB),NOSPLIT,$0
	JMP	runtime∕internal∕atomic·Xadd64(SB)

注释解释了汇编语言,将给定地址中的值加上一个指定的增量,并返回增加前的值

// uint32 Xadd(uint32 volatile *val, int32 delta)
// Atomically:
//	*val += delta;
//	return *val;
TEXT ·Xadd(SB), NOSPLIT, $0-20
	MOVQ	ptr+0(FP), BX
	MOVL	delta+8(FP), AX
	MOVL	AX, CX
	LOCK
	XADDL	AX, 0(BX)
	ADDL	CX, AX
	MOVL	AX, ret+16(FP)
	RET

eg:
  在下面的例子中,用go协程模拟并发安全性问题,其中unsafe是不安全的,因为sum += i 并不是原子性操作,所以在并发情况下会导致数据不一致,在safe中解决了此问题,把sum += i 换成了atomic.AddInt32(&sum, i),因为他是原子性操作,所以不会有并发问题,当然了也可以使用mutex互斥锁来解决此问题,他保证在任意时刻只有一个 goroutine 可以访问临界区

var value int32
var wg sync.WaitGroup

func main() {
	unsafe()
	safe()
}
func unsafe() {
	var sum int32 = 0
	N := 100
	wg.Add(N)
	for i := 0; i < N; i++ {
		go func(i int32) {
			sum += i
			wg.Done()
		}(int32(i))
	}
	wg.Wait()
	fmt.Printf("unsafe:%d以内所有的数字之和为:%d\n", N, sum)
}
func safe() {
	var sum int32 = 0
	N := 100
	wg.Add(N)
	for i := 0; i < N; i++ {
		go func(i int32) {
			atomic.AddInt32(&sum, i)
			wg.Done()
		}(int32(i))
	}
	wg.Wait()
	fmt.Printf("safe:%d以内所有的数字之和为:%d", N, sum)
}

运行结果:unsafe的结果在多次操作后可能不是正确的数据,他的数据具有不确定性

unsafe:100以内所有的数字之和为:4882
safe:100以内所有的数字之和为:4950

Load(原子读取)

  在进行大量的读写操作的时候,读取一个变量,很有可能这个变量正在被写入,有可能会读取到错误的数据,即这个数据刚写入一半,这时可以使用原子读取Load进行读取

func LoadInt32(addr *int32) (val int32)

func LoadInt64(addr *int64) (val int64)

func LoadUint32(addr *uint32) (val uint32)

func LoadUint64(addr *uint64) (val uint64)

func LoadUintptr(addr *uintptr) (val uintptr)

func LoadPointer(addr *unsafe.Pointer)	 (val unsafe.Pointer)

下面是汇编代码,只需了解即可,大概意思就是对六种数据类型进行原子性读读取。


TEXT ·Loaduintptr(SB), NOSPLIT, $0-16
	JMP	·Load64(SB)

TEXT ·Loaduint(SB), NOSPLIT, $0-16
	JMP	·Load64(SB)

TEXT ·Loadint32(SB), NOSPLIT, $0-12
	JMP	·Load(SB)

TEXT ·Loadint64(SB), NOSPLIT, $0-16
	JMP	·Load64(SB)

eg:

var counter int32
var wg sync.WaitGroup

func main() {
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go doTask()
	}
	wg.Wait()
	fmt.Println("Counter Value:", atomic.LoadInt32(&counter))
}

运行结果: Counter Value: 1000

store操作

有读取肯定有写入操作,这个写入操作也是原子性的,直接操作的是硬件读取。

func StoreInt32(addr *int32, val int32)

func StoreInt64(addr *int64, val int64)

func StoreUint32(addr *uint32, val uint32)

func StoreUint64(addr *uint64, val uint64)

func StoreUintptr(addr *uintptr, val uintptr)

func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

下面是汇编语言的源代码,这些函数通过汇编指令 XCHG 来实现原子性的存储操作。这样可以确保在多个goroutine在同一时间内,并发访问同一个内存地址时,不会出现竞态条件和数据不一致的问题

TEXT ·StorepNoWB(SB), NOSPLIT, $0-16
	MOVQ	ptr+0(FP), BX
	MOVQ	val+8(FP), AX
	XCHGQ	AX, 0(BX)
	RET

TEXT ·Store(SB), NOSPLIT, $0-12
	MOVQ	ptr+0(FP), BX
	MOVL	val+8(FP), AX
	XCHGL	AX, 0(BX)
	RET

TEXT ·Store8(SB), NOSPLIT, $0-9
	MOVQ	ptr+0(FP), BX
	MOVB	val+8(FP), AX
	XCHGB	AX, 0(BX)
	RET

TEXT ·Store64(SB), NOSPLIT, $0-16
	MOVQ	ptr+0(FP), BX
	MOVQ	val+8(FP), AX
	XCHGQ	AX, 0(BX)
	RET

TEXT ·Storeint32(SB), NOSPLIT, $0-12
	JMP	·Store(SB)

TEXT ·Storeint64(SB), NOSPLIT, $0-16
	JMP	·Store64(SB)

TEXT ·Storeuintptr(SB), NOSPLIT, $0-16
	JMP	·Store64(SB)

TEXT ·StoreRel(SB), NOSPLIT, $0-12
	JMP	·Store(SB)

TEXT ·StoreRel64(SB), NOSPLIT, $0-16
	JMP	·Store64(SB)

TEXT ·StoreReluintptr(SB), NOSPLIT, $0-16
	JMP	·Store64(SB)

eg:下面是使用store的例子

var wg sync.WaitGroup

func main() {
	var value int32
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go doAtomicStore(&value, int32(i))
	}
	wg.Wait()
	loadedValue := atomic.LoadInt32(&value)
	fmt.Println("Final value:", loadedValue)
}

func doAtomicStore(ptr *int32, val int32) {
	// 使用原子存储函数将值存储到共享变量中
	atomic.StoreInt32(ptr, val)
	wg.Done()
}

运行结果:Final value: 98

Value的读操作

  这个相信大家在go的标准库中经常见到,比如Golang的context库中done的实现,在上章节讲解的源码中有分析,context用atomic.value 来存储一个类型为 <-chan struct{} 的通道的原子值.

  value 类型用来存储任意类型的数据相比之前的存储方式,非常的简单粗暴。需要注意的是,一但第一次store写入的类型就会确定,如果使用的类型不一致会导致panic,efaceWords其实就是一个空interface,相当于eface想要深入了解,还需先了解一下unsafe.Pointer

   unsafe.Pointer: 它是go的一个特殊类型,他可以表示任意类型的指针,需要注意的是,他在编译时并不会对数据类型进行检查,所以使用他的时候要特别小心。它可以用来进行类型转换,在转换时一定要检查是否具有相同的内存结构,下面的代码吧byte类型零值拷贝成了string,这样的性能明显高于标准转化即string(bytes)

func main() {
	bytes := []byte{101, 102, 108, 108, 111}
	a := unsafe.Pointer(&bytes)
	str := *(*string)(a)
	fmt.Println(str)
}

运行结果:Hello, Golang!

了解了unsafe.Pointer之后可以对Store进行了解了,

type Value struct {
	v any
}

type efaceWords struct {
	typ  unsafe.Pointer
	data unsafe.Pointer
}

Srore操作

其实store还是基于LoadPointer实现的,所以他的性能略低于Load文章来源地址https://www.toymoban.com/news/detail-833883.html

func (v *Value) Store(val any) {
	if val == nil {
		panic("sync/atomic: store of nil value into Value")
	}
	vp := (*efaceWords)(unsafe.Pointer(v))
	vlp := (*efaceWords)(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.
			runtime_procPin()
			if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {
				runtime_procUnpin()
				continue
			}
			// Complete first store.
			StorePointer(&vp.data, vlp.data)
			StorePointer(&vp.typ, vlp.typ)
			runtime_procUnpin()
			return
		}
		if typ == unsafe.Pointer(&firstStoreInProgress) {
			// 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
	}
}
  • 首先先判断val ,如果val为空直接panic,存的值类型不能为空。
  • unsafe.Pointer将现有的和要写入的值分别转成ifaceWords类型,这样我们下一步就可以得到这两个interface{}的原始类型(typ)和真正的值(data)
  • 通过一个无限for循环,配合CompareAndSwap来实现乐观锁,检查 vp 中存储的类型信息是否为 nil,如果是,则表示尚未进行任何存储操作,可以尝试开始第一次存储操作,后给type设置一个标识位,来表示有goroutine正在写入,然后退出
  • runtime_procPin() 是gmp中的p,这个方法就是在这个过程中不允许其他m和g抢占
  • 如果type不为nil,但是等于标识位,表示有正在写入的goroutine,然后继续循环
  • 如果 vp 中存储的类型信息与 new 中存储的类型信息不一致,则抛出 panic。这是为了防止不一致类型的值被存储到 Value 中
  • 最后类型已经写入,直接保存数据

Load操作

func (v *Value) Load() (val any) {
	vp := (*efaceWords)(unsafe.Pointer(v))
	typ := LoadPointer(&vp.typ)
	if typ == nil || typ == unsafe.Pointer(&firstStoreInProgress) {
		// First store not yet completed.
		return nil
	}
	data := LoadPointer(&vp.data)
	vlp := (*efaceWords)(unsafe.Pointer(&val))
	vlp.typ = typ
	vlp.data = data
	return
}
  • 读取也是基于Load实现的,将 v 转换为 efaceWords 结构体的指针 vp
  • 如果 typ 为 nil 或者等于 firstStoreInProgress 标记,则表示尚未进行任何存储操作,直接返回 nil
  • 如果 typ 不为空,则表示已经进行了存储操作,继续加载存储的数据
  • 否则,根据当前看到的typ和data构造出一个新的interface{}返回出去

到了这里,关于深入了解Golang atomic原子操作的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Golang星辰图】Go语言云计算SDK全攻略:深入Go云存储SDK实践

    在当今数字化时代,云计算和存储服务扮演着至关重要的角色,为应用程序提供高效、可靠的基础设施支持。本文将介绍几种流行的Go语言SDK,帮助开发者与AWS、Google Cloud、Azure、MinIO、 阿里云和腾讯云等各大云服务提供商的平台进行交互。 欢迎订阅专栏:Golang星辰图 1.1 提供

    2024年03月17日
    浏览(52)
  • 面试专题:java多线程(3)---关于 Atomic 原子类

    1.介绍一下Atomic 原子类Atomic     翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里  Atomic   是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰

    2024年02月07日
    浏览(58)
  • C++ 多线程:原子操作atomic

    C++ 多线程:原子类型 有两个线程,一个要写数据,一个读数据,如果不加锁,可能会造成读写值混乱,使用 std::mutex 程序执行不会导致混乱, 但是每一次循环都要加锁解锁是的程序开销很大。 为了提高性能,C++11提供了原子类型( std::atomicT ),它提供了多线程间的原子操作,

    2024年02月13日
    浏览(41)
  • C++并发编程 | 原子操作std::atomic

    目录 1、原子操作std::atomic相关概念 2、不加锁情况 3、加锁情况  4、原子操作 5、总结 原子操作: 更小的代码片段,并且该片段必定是连续执行的,不可分割。 1.1 原子操作std::atomic与互斥量的区别 1) 互斥量 :类模板,保护一段共享代码段,可以是一段代码,也可以是一个

    2023年04月26日
    浏览(37)
  • Go For Web:一篇文章带你用 Go 搭建一个最简单的 Web 服务、了解 Golang 运行 web 的原理

    本文作为解决如何通过 Golang 来编写 Web 应用这个问题的前瞻,对 Golang 中的 Web 基础部分进行一个简单的介绍。目前 Go 拥有成熟的 Http 处理包,所以我们去编写一个做任何事情的动态 Web 程序应该是很轻松的,接下来我们就去学习了解一些关于 Web 的相关基础,了解一些概念,

    2023年04月14日
    浏览(51)
  • 100天精通Golang(基础入门篇)——第12天:深入解析Go语言中的集合(Map)及常用函数应用

    🌷 博主 libin9iOak带您 Go to Golang Language.✨ 🦄 个人主页——libin9iOak的博客🎐 🐳 《面试题大全》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 🌊 《IDEA开发秘籍》学会IDEA常用操作,工作效率翻倍~💐 🪁 希望本文能够给您带来一定的帮助🌸文章粗浅,敬请批

    2024年02月12日
    浏览(50)
  • Golang 通过开源库 go-redis 操作 NoSQL 缓存服务器

    前置条件: 1、导入库: import ( \\\"github.com/go-redis/redis/v8\\\" ) 2、搭建哨兵模式集群 具体可以百度、谷歌搜索,网上现成配置教程太多了,不行还可以搜教程视频,跟着视频博主一步一个慢动作,慢慢整。 本文只介绍通过 “主从架构 / 哨兵模式” 访问的形式,这是因为,单个

    2024年01月23日
    浏览(51)
  • 【C++入门到精通】 原子性操作库(atomic) C++11 [ C++入门 ]

    当谈及并发编程时,确保数据的安全性和一致性是至关重要的。在C++11中引入的原子性操作库(atomic)为我们提供了一种有效且可靠的方式来处理多线程环境下的数据共享与同步问题。原子操作是不可分割的操作,它们可以确保在多线程环境中对共享数据的读写操作是原子的

    2024年02月03日
    浏览(40)
  • 100天精通Golang(基础入门篇)——第15天:深入解析Go语言中函数的应用:从基础到进阶,助您精通函数编程!(进阶)

    🌷 博主 libin9iOak带您 Go to Golang Language.✨ 🦄 个人主页——libin9iOak的博客🎐 🐳 《面试题大全》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 🌊 《IDEA开发秘籍》学会IDEA常用操作,工作效率翻倍~💐 🪁 希望本文能够给您带来一定的帮助🌸文章粗浅,敬请批

    2024年02月12日
    浏览(68)
  • 【Golang】Golang进阶系列教程--Go 语言 map 如何顺序读取?

    Go 语言中的 map 是一种非常强大的数据结构,它允许我们快速地存储和检索键值对。 然而,当我们遍历 map 时,会有一个有趣的现象,那就是输出的键值对顺序是不确定的。 先看一段代码示例: 当我们多执行几次这段代码时,就会发现,输出的顺序是不同的。 首先,Go 语言

    2024年02月14日
    浏览(69)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包