Golang教程四(协程,channel,线程安全,syncMap)

发布于:2024-04-12 ⋅ 阅读:(69) ⋅ 点赞:(0)

目录

一、Goroutine和channel

Goroutine

Channel

发送和接收操作

缓冲 Channel

关闭 Channel

使用 Channel 进行同步

Select语句

协程超时处理 

方法一:使用context.Context

 方法二:使用time.Timer/time.Ticker

 二、线程安全与sync.Map

线程安全

 sync.RWMutex(互斥锁)

sync.RWMutex (读写互斥锁)

sync.WaitGroup

sync.Once

sync.Cond

sync.map

sync.atomic


一、Goroutine和channel

Goroutine

Goroutine是Go语言中实现并发的一种轻量级线程,由Go运行时管理。Goroutines与操作系统线程相比,具有更低的创建和切换开销,使得Go语言能够轻松地实现高并发编程。

创建 Goroutine: 在Go语言中,使用关键字 go 与函数调用结合即可创建一个Goroutine

Goroutine间的通信: Go语言提倡通过 channels(通道)进行Goroutine间的通信和同步。Channels是类型化的管道,可以发送和接收指定类型的值。通过 channels,Goroutines可以安全地共享和交换数据,避免数据竞争和竞态条件(达到线程安全的目的)。

package main

import (
	"fmt"
	"time"
)

func main() {

	ch := make(chan int)

	go func() {
		time.Sleep(1 * time.Second)
		ch <- 42 // 发送一个整数到channel ch
	}()

	fmt.Println("准备输出:")
	result := <-ch      // 从channel ch 接收一个整数
	fmt.Println(result) // 输出:42
}

Channel

在Go语言中,通道(Channel)是一种核心的并发原语,用于在Goroutines之间进行通信和同步。通道是类型化的,只能发送和接收指定类型的值。

通过 Channel,可以有效地协调并发任务,确保数据正确流动,构建复杂的并发程序。理解和熟练使用 Channel 是编写高效、安全的 Go 语言代码的关键。

发送和接收操作

向 Channel 发送值使用 <- 运算符左侧,接收值使用 <- 运算符右侧:

ch := make(chan int)

go func() {
    ch <- 42 // 向 Channel ch 发送整数 42
}()

value := <-ch // 从 Channel ch 接收一个整数,并赋值给变量 value
fmt.Println(value) // 输出:42

缓冲 Channel

通过在 make 函数中指定第二个参数,可以创建带缓冲的 Channel。缓冲 Channel 允许在没有接收者时暂存一定数量的数据,直到被接收。缓冲大小为指定的整数值:

bufferedCh := make(chan int, 5) // 创建一个缓冲大小为 5 的整数 Channel

关闭 Channel

使用 close 函数关闭 Channel,表示不会再有新的值发送到该 Channel。关闭 Channel 后,接收操作会继续接收剩余的值,直到 Channel 空。尝试向已关闭的 Channel 发送值会导致 panic。接收操作可以从已关闭的 Channel 接收值,直到 Channel 空。若 Channel 已关闭且为空,接收操作会立即返回零值,并且 ok 参数(如 value, ok := <-ch)设为 false,表明 Channel 已关闭且无值可接收。

close(ch) // 关闭 Channel ch

使用 Channel 进行同步

Channel 除了用于数据传输,还能作为同步机制。通过发送和接收操作的阻塞特性,可以实现 Goroutine 间的同步和协作:

doneCh := make(chan struct{})
go func() {
    // 执行任务...
    close(doneCh) // 任务完成时关闭 doneCh
}()

<-doneCh // 阻塞,等待 doneCh 被关闭,即任务完成

Select语句

select 语句用于监听多个 Channel 的发送或接收操作,当任意一个操作可以进行时,select 语句会选择一个执行。select 语句支持 default 子句,当所有 Channel 都不可操作时执行。

ch1 := make(chan int)
ch2 := make(chan string)

select {
case i := <-ch1:
    fmt.Println("Received from ch1:", i)
case s := <-ch2:
    fmt.Println("Received from ch2:", s)
default:
    fmt.Println("Both channels are empty")
}

协程超时处理 

处理协程(Goroutine)超时通常有两种常用方法:使用context.Context和使用time.Timer/time.Ticker。以下是这两种方法的详细介绍

方法一:使用context.Context

context.Context 是 Go 语言中用于在多个 Goroutine 之间传播截止时间、取消信号以及携带请求范围内的上下文信息的标准方式。要实现协程超时,可以创建一个带超时时间的context.Context,并将它传递给协程执行的函数。当超时时间到达时,Context 会触发取消信号,通知协程停止执行。

package main

import (
	"context"
	"fmt"
	"time"
)

func doWork(ctx context.Context) error {
	// 在循环、IO操作等处定期检查 ctx.Done(),一旦返回非nil,说明超时或被取消
	for {
		select {
		case <-ctx.Done():
			return ctx.Err() // 返回一个表示超时或取消的错误
		default:
			// 正常执行任务
		}
	}
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // 创建一个5秒后超时的Context
	defer cancel()                                                          // 及时取消Context,释放资源

	go func() {
		if err := doWork(ctx); err != nil {
			fmt.Println("doWork failed:", err)
		}
	}()

	// 其他操作
	for i := 0; i < 6; i++ {
		time.Sleep(1 * time.Second)
		fmt.Printf("doWork is running... %d \n", i)
	}

}

 方法二:使用time.Timer/time.Ticker

time.Timer 和 time.Ticker 提供了基于时间的定时器功能。Timer 用于一次性触发,而 Ticker 用于周期性触发。可以创建一个 Timer,并在其到期时取消正在执行的协程。

package main

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

func doWork(stopCh chan struct{}) error {
	// 在循环、IO操作等处定期检查 stopCh,一旦收到信号,说明需要停止
	for {
		select {
		case <-stopCh:
			return nil // 返回表示被停止的错误
		default:
			// 正常执行任务
		}
	}
}

func main() {
	stopCh := make(chan struct{})
	var wg sync.WaitGroup
	wg.Add(1)

	go func() {
		defer wg.Done()
		if err := doWork(stopCh); err != nil {
			fmt.Println("doWork failed:", err)
		}
	}()

	timer := time.NewTimer(5 * time.Second) // 创建一个5秒后到期的Timer
	go func() {
		<-timer.C     // 等待Timer到期
		close(stopCh) // 发送停止信号
	}()

	wg.Wait() // 等待doWork协程完成
}

 二、线程安全与sync.Map

线程安全

Golang 中的线程安全是指编写并发程序时,确保即使在多个并发执行的 Goroutines(Go 语言中的轻量级线程)同时访问和修改共享数据时,也能保持程序的正确性、一致性和完整性,避免数据竞争、竞态条件等并发问题的发生。

以下是关于 Golang 线程安全的详细说明:

  1. 共享状态与数据竞争: 在并发环境中,当多个 Goroutines 访问和/或修改相同的内存区域(如全局变量、共享数据结构等)时,就存在共享状态。如果没有采取适当的同步措施,不同的 Goroutine 可能会交错执行,导致数据的非预期更新,这就是所谓的数据竞争。例如,多个 Goroutine 同时对一个计数器进行递增操作可能会导致最终计数值小于实际递增次数。

  2. 同步原语与机制: Go 语言提供了丰富的同步原语和机制来实现线程安全:

    • 互斥锁 (sync.Mutex 和 sync.RWMutex): 互斥锁用于保护临界区,确保同一时刻只有一个 Goroutine 能访问受保护的资源。sync.Mutex 提供独占锁,当一个 Goroutine 获取锁后,其他尝试获取锁的 Goroutines 将被阻塞,直到锁被释放。sync.RWMutex 是读写锁,支持多个读取者并发访问,但在写入时会阻塞所有读取者和其他写入者。

    • 通道 (chan): 通道是 Go 语言的核心并发原语,用于在 Goroutines 之间传递数据。通道的发送和接收操作是同步的,即发送操作会阻塞直到有接收者接收数据,反之亦然。通过使用通道,可以实现 Goroutines 之间的同步和通信,避免直接共享数据带来的线程安全问题。

    • 其他同步工具: sync 包还提供了其他同步工具,如:

      • sync.WaitGroup:用于等待一组 Goroutines 完成其任务。
      • sync.Once:确保某个初始化动作只被执行一次,即使在并发环境中。
      • sync.Cond:允许 Goroutines 在满足特定条件时进行通知和等待。
    • 原子操作 (sync/atomic): sync/atomic 包提供了原子级别的整数和指针操作,这些操作在硬件级别上保证了其不可中断性,即使在并发环境中也能确保数据的一致性,无需使用锁。例如,可以使用 atomic.AddInt64 来实现无锁的整数递增。

  3. 线程安全的数据结构: Go 标准库中包含了一些线程安全的数据结构,如 sync.Mapsync.Map 是一个并发安全的映射(map),内部已经实现了适当的锁机制,使得多个 Goroutines 可以安全地并发读写。

实现 Golang 线程安全的关键在于正确地使用上述同步原语来管理对共享数据的访问。这通常包括确定哪些数据需要同步、选择合适的同步机制(如互斥锁、通道、原子操作等)、遵循最小化同步区域的原则以提高并发性能,并避免死锁等并发问题。

总之,Golang 中的线程安全是通过使用语言提供的同步原语和机制,确保在并发环境下对共享数据的操作是有序且一致的,从而防止数据竞争、竞态条件等并发问题,保证程序的正确运行。

 sync.RWMutex(互斥锁

关键特性

  1. 互斥性sync.Mutex 保证了同一时间只有一个 Goroutine 能持有锁,即只有一个 Goroutine 能进入临界区。其他尝试获取锁的 Goroutines 将被阻塞,直到锁被持有锁的 Goroutine 释放。

  2. 重入:Go 的互斥锁不支持重入,即已经持有锁的 Goroutine 不能再次获取同一把锁。如果尝试这样做,程序将引发 panic。

  3. 锁定与解锁:互斥锁有两个主要方法:

    • Lock():获取锁。如果锁当前未被持有,调用者获得锁并继续执行;如果锁已被持有,调用者将被阻塞,直到锁被释放。
    • Unlock():释放锁。只有持有锁的 Goroutine 才能成功释放锁。释放后,如果有其他等待的 Goroutine,其中一个将被唤醒并获得锁。
  4. 零值有效性sync.Mutex 的零值就是一个有效的、未锁定的互斥锁,无需显式初始化即可使用。

package main

import (
	"fmt"
	"sync"
)

var (
	value int
	wait  sync.WaitGroup
	mu    sync.Mutex // 保护 value 字段的互斥锁
)

func increment() {
	defer wait.Done()
	mu.Lock() // 获取锁
	value++
	mu.Unlock() // 释放锁
}

// 如果不加锁,结果是随机的
// 根本原因是CPU的调度方法为抢占式执行,随机调度
func main() {
	for i := 0; i < 1000; i++ {
		wait.Add(1)
		go increment()
	}
	wait.Wait()
	fmt.Println(value)
}

注意事项

  1. 不要忘记解锁:长时间持有锁会导致其他 Goroutines 长期阻塞,降低程序的并发性能。确保及时释放不再需要的锁。

  2. 不要重复锁定:一个 Goroutine 不能对已持有的互斥锁再次调用 Lock()。重复锁定会导致 panic。

  3. 避免解锁未锁定的锁:调用 Unlock() 之前必须确保锁已被持有。对未锁定的锁进行解锁同样会引发 panic。

  4. 合理组织临界区:尽量缩小临界区的范围,仅包含真正需要同步的代码,以减少锁的争用和提高并发效率。

sync.RWMutex (读写互斥锁)

sync.RWMutex 是 Go 语言标准库 sync 包中提供的读写互斥锁类型,它扩展了基本互斥锁(sync.Mutex)的功能,专为处理读多写少的并发场景。sync.RWMutex 允许任意数量的 Goroutines 同时进行读操作,但在进行写操作时,会阻止所有读取者和写入者,以保证数据的完整性。

关键特性与方法

  1. 读锁(共享锁):多个 Goroutine 可以同时持有读锁,这意味着它们可以并发地进行读操作。读锁由 RLock() 方法获取,由 RUnlock() 方法释放。

  2. 写锁(排他锁):任何时候只能有一个 Goroutine 持有写锁。当有 Goroutine 持有写锁时,其他所有试图获取读锁或写锁的 Goroutines 都将被阻塞。写锁由 Lock() 方法获取,由 Unlock() 方法释放。

  3. 零值有效性:同 sync.Mutexsync.RWMutex 的零值也是一个有效的、未锁定的读写互斥锁。

package main

import "sync"

type SharedCounter struct {
	count int
	mu    sync.RWMutex
}

func (c *SharedCounter) ReadCount() int {
	c.mu.RLock()         // 获取读锁
	defer c.mu.RUnlock() // 释放读锁

	return c.count
}

func (c *SharedCounter) Increment() {
	c.mu.Lock()         // 获取写锁
	defer c.mu.Unlock() // 释放写锁

	c.count++
}

//在这个例子中:
//ReadCount 方法使用 RLock() 获取读锁,允许多个 Goroutine 并发读取 count 值,提高了读取操作的并发性能。
//Increment 方法使用 Lock() 获取写锁,确保在递增 count 的过程中,没有其他 Goroutine(无论是读还是写)能访问 count,保证了数据更新的原子性。

 注意事项

  1. 读写平衡:虽然 sync.RWMutex 支持并发读,但如果写操作过于频繁或持有写锁的时间过长,可能会导致大量读取 Goroutines 长时间阻塞,降低系统的整体并发性能。因此,应合理设计数据结构和算法,尽量减少写操作的频率和持续时间。

  2. 避免死锁:与 sync.Mutex 类似,确保每个 Lock()RLock() 操作都有对应的 Unlock()RUnlock(),且在正确的代码路径上执行。使用 defer 可以简化这一过程。

  3. 不要忘记解锁:长时间持有锁(无论是读锁还是写锁)都会影响其他 Goroutines 的执行。确保及时释放不再需要的锁。

  4. 不要重复锁定:一个 Goroutine 不能对已持有的锁(无论读锁还是写锁)再次调用 Lock() 或 RLock()。重复锁定会导致 panic。

  5. 避免解锁未锁定的锁:调用 Unlock() 或 RUnlock() 之前必须确保相应的锁已被持有。对未锁定的锁进行解锁同样会引发 panic。

总结来说,sync.RWMutex 是 Go 语言中针对读多写少场景优化的同步原语,通过区分读锁和写锁,允许并发读取以提高系统性能,同时在写操作时保证数据一致性。在设计并发程序时,根据数据的访问模式合理选择和使用 sync.Mutex 或 sync.RWMutex,有助于构建高效且线程安全的并发代码。

sync.WaitGroup

sync.WaitGroup 是 Go 语言标准库 sync 包中提供的一个类型,用于同步一组 Goroutines 的执行,确保所有 Goroutines 完成其任务后,主线程或其他相关 Goroutines 才能继续执行后续操作。它常用于并行计算、批处理任务、网络请求聚合等场景。

sync.WaitGroup 主要包含以下方法:

  1. Add(delta int)

    将 WaitGroup 的计数器增加 delta。通常在启动 Goroutine 前调用,传入 delta=1 表示增加一个待完成的任务。
  2. Done()

    将 WaitGroup 的计数器减一。当一个 Goroutine 完成其任务时,应调用此方法。相当于 Add(-1)
  3. Wait()

    阻塞当前 Goroutine,直到 WaitGroup 的计数器变为零,即所有待完成的任务都已经调用了 Done()。这意味着所有关联的 Goroutines 已经完成其任务。
package main

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

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done() // Goroutine 完成时,通过 defer 语句确保调用 Done()

	fmt.Printf("Worker %d started\n", id)
	time.Sleep(time.Second * time.Duration(id))
	fmt.Printf("Worker %d finished\n", id)
}

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 3; i++ {
		wg.Add(1) // 启动一个 Goroutine 前,增加 WaitGroup 计数器
		go worker(i, &wg)
	}

	fmt.Println("Waiting for workers to finish...")
	wg.Wait() // 阻塞主线程,直到所有 worker Goroutines 完成

	fmt.Println("All workers have finished.")
}

在这个例子中,我们使用 sync.WaitGroup 同步三个 worker Goroutines 的执行。在启动每个 Goroutine 之前,通过 wg.Add(1) 增加 WaitGroup 的计数器。每个 worker Goroutine 在完成任务后调用 wg.Done()。主线程在 wg.Wait() 处阻塞,直到所有 worker Goroutines 都调用了 Done(),即所有任务都已完成,然后继续执行后续代码。

总结来说,sync.WaitGroup 是 Go 语言中用于同步一组 Goroutines 执行的工具,它通过计数器跟踪待完成的任务数量,确保所有 Goroutines 完成其任务后,主线程或其他相关 Goroutines 才能继续执行。通过使用 sync.WaitGroup.Add()sync.WaitGroup.Done() 和 sync.WaitGroup.Wait(),可以方便地管理一组并发任务的执行,确保任务的正确完成和程序的正确同步。

sync.Once

sync.Once 是 Go 语言标准库 sync 包中提供的一个类型,用于确保某个初始化动作只被执行一次,即使在并发环境中。它常用于初始化全局变量、连接数据库、加载配置文件等只需进行一次的初始化操作。使用 sync.Once 可以确保在多线程或 Goroutine 并发访问时,这些操作不会被重复执行,从而避免资源浪费、数据不一致等问题。

sync.Once 主要包含两个方法:

  1. Do(f func())

    • 参数 f 是一个无参数、无返回值的函数,代表需要只执行一次的初始化动作。
    • 当首次调用 Do(f) 时,f 会被执行。之后对 Do(f) 的任何调用都不会再执行 f
    • Do(f) 方法是线程安全的,即使在多个 Goroutine 并发调用的情况下,也能确保 f 只被执行一次。
  2. Done()

    • 返回一个只读的布尔值,表示初始化动作是否已经完成。
    • 通常情况下,你不需要直接调用 Done(),因为它主要用于测试和调试。
package main

import (
	"fmt"
	"sync"
)

var once sync.Once
var message string

func initMessage() {
	message = "Hello, World!"
	fmt.Println("Initialization performed.")
}

func GetMessage() string {
	once.Do(initMessage)
	return message
}

func main() {
	go func() {
		fmt.Println(GetMessage())
	}()
	go func() {
		fmt.Println(GetMessage())
	}()

	// 程序在此处阻塞,等待 Goroutines 完成
	var input string
	fmt.Scanln(&input)
}

sync.Cond

sync.Cond 是 Go 语言标准库 sync 包中提供的一个条件变量类型,用于协调多个 Goroutines 的执行。条件变量允许 Goroutines 在满足特定条件时进行通知和等待,常用于解决生产者-消费者模型、工作池、资源池等场景中的同步问题。

sync.Cond 主要包含以下方法:

  1. NewCond(l Locker)

    • 创建一个新的 sync.Cond 对象,需要传入一个实现了 sync.Locker 接口(如 sync.Mutex 或 sync.RWMutex)的对象作为底层锁。所有与 sync.Cond 相关的操作都需要在锁的保护下进行。
  2. Wait()

    • 调用 Wait() 的 Goroutine 会释放底层锁,并进入等待状态,直到收到信号或广播通知。收到通知后,Goroutine 会重新获取底层锁并继续执行。
  3. Signal()

    • 发送一个信号给等待队列中的一个 Goroutine。被唤醒的 Goroutine 将重新获取底层锁并继续执行。如果此时没有 Goroutine 在等待,信号将被忽略。
  4. Broadcast()

    • 向等待队列中的所有 Goroutine 发送广播通知。所有被唤醒的 Goroutine 将重新获取底层锁并继续执行。

使用场景

sync.Cond 经常用在多个 Goroutine 等待,一个 Goroutine 通知(事件发生)的场景。如果是一个通知,一个等待,使用互斥锁或 channel 就能搞定了。

我们想象一个非常简单的场景:

有一个协程在异步地接收数据,剩下的多个协程必须等待这个协程接收完数据,才能读取到正确的数据。在这种情况下,如果单纯使用 chan 或互斥锁,那么只能有一个协程可以等待,并读取到数据,没办法通知其他的协程也读取数据。

这个时候,就需要有个全局的变量来标志第一个协程数据是否接受完毕,剩下的协程,反复检查该变量的值,直到满足要求。或者创建多个 channel,每个协程阻塞在一个 channel 上,由接收数据的协程在数据接收完毕后,逐个通知。总之,需要额外的复杂度来完成这件事。

Go 语言在标准库 sync 中内置一个 sync.Cond 用来解决这类问题。

 原理参考:Golang sync.Cond 简介与用法-CSDN博客

package main

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

var done = false

func read(name string, c *sync.Cond) {
	c.L.Lock()
	for !done {
		c.Wait()
	}
	fmt.Println(name, "starts reading")
	c.L.Unlock()
}

func write(name string, c *sync.Cond) {
	fmt.Println(name, "starts writing")
	time.Sleep(3 * time.Second)
	done = true
	fmt.Println(name, "wakes all")
	c.Broadcast()
}

func main() {
	cond := sync.NewCond(&sync.Mutex{})

	go read("reader1", cond)
	go read("reader2", cond)
	go read("reader3", cond)
	write("writer", cond)

	time.Sleep(time.Second * 10)
}

sync.map

sync.Map 是 Go 语言标准库 sync 包中提供的一个并发安全的映射(map)类型,专为多线程环境设计,允许多个 Goroutines 并发地读取和写入数据,而无需手动进行同步。sync.Map 内部实现了适当的锁机制和数据分段技术,能够在保证线程安全的同时,提供比使用互斥锁(如 sync.Mutex)保护普通映射(map)更好的性能。

sync.Map 主要包含以下方法:

  1. Load(key interface{}) (value interface{}, ok bool)

    从映射中查找给定的 key,返回对应的 value 和一个布尔值 ok,表示查找是否成功。如果 key 不存在,value 为对应类型的零值,ok 为 false。读取操作对并发友好,即使在写入操作正在进行时也可以安全地进行。
  2. Store(key, value interface{})

    将给定的 key-value 对存储到映射中。如果 key 已存在,则更新其对应的 value。写入操作可能会阻塞其他写入操作,但不会阻塞读取操作。
  3. Delete(key interface{})

    从映射中删除给定的 key 及其对应的 value。如果 key 不存在,此操作无任何效果。删除操作可能会阻塞其他写入操作,但不会阻塞读取操作。
  4. Range(f func(key, value interface{}) bool)

    遍历映射中的所有 key-value 对。f 是一个回调函数,对每个 key-value 对调用一次。如果 f 返回 false,则停止遍历。遍历操作在遍历期间会锁定整个映射,阻止其他写入操作,但允许并发读取。
  5. Len() int

    返回映射中元素的数量。由于计数可能在并发操作下发生变化,这个值只能视为近似值。
package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map

	m.Store("key1", "value1")
	m.Store("key2", "value2")

	// 读取操作
	value, ok := m.Load("key1")
	if ok {
		fmt.Println("Value for key1:", value.(string)) // 输出:Value for key1: value1
	}

	// 删除操作
	m.Delete("key2")

	// 遍历操作
	m.Range(func(key, value interface{}) bool {
		fmt.Println("Key:", key, "Value:", value)
		return true // 继续遍历
	})

	// 更新操作
	m.Store("key1", "new_value1")

	// 读取新值
	value, ok = m.Load("key1")
	if ok {
		fmt.Println("Updated value for key1:", value.(string)) // 输出:Updated value for key1: new_value1
	}
}

在这个例子中,我们展示了 sync.Map 的基本使用方法,包括存储、读取、删除、遍历和更新操作。通过使用 sync.Map,可以在并发环境中安全地操作映射数据,无需担心数据竞争和竞态条件。

总结来说,sync.Map 是 Go 语言中用于实现并发安全映射的类型,它内部实现了高效的锁机制和数据分段技术,使得多个 Goroutines 可以安全地并发读取和写入数据。通过使用 sync.Map.Load()sync.Map.Store()sync.Map.Delete()sync.Map.Range() 等方法,可以轻松地在并发环境中管理映射数据,确保程序的正确性和数据一致性。

sync.atomic

sync/atomic 是 Go 语言标准库中的一个包,提供了对整数和指针的原子操作支持。原子操作在硬件级别上保证了操作的不可分割性,即在操作完成之前,其他并发的 Goroutines 无法观察到操作的中间状态。使用 sync/atomic 包中的函数可以确保在并发环境中对整数或指针的读取、修改等操作是原子的,从而避免数据竞争和竞态条件,保障程序的线程安全性。

sync/atomic 包主要包含以下类型和函数:

原子整数类型

  • int32
  • uint32
  • int64
  • uint64
  • uintptr

原子操作函数

  • 原子读取LoadUint32(addr *uint32) (val uint32)LoadInt64(addr *int64) (val int64) 等,用于原子地读取整数或指针的值。
  • 原子存储StoreUint32(addr *uint32, val uint32)StoreInt64(addr *int64, val int64) 等,用于原子地将整数或指针的值设置为给定值。
  • 原子交换SwapUint32(addr *uint32, new uint32) (old uint32)SwapInt64(addr *int64, new int64) (old int64) 等,用于原子地将整数或指针的值替换为给定的新值,并返回旧值。
  • 原子加法AddUint32(addr *uint32, delta uint32) (new uint32)AddInt64(addr *int64, delta int64) (new int64) 等,用于原子地将整数或指针的值增加指定的增量,并返回新的值。
  • 原子比较并交换(CAS):CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool) 等,如果当前值等于 old,则将整数或指针的值设置为 new,并返回 true;否则不改变值,返回 false
package main

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

func main() {
	var counter uint32 = 0

	go func() {
		for i := 0; i < 10000; i++ {
			atomic.AddUint32(&counter, 1) // 原子递增 counter
		}
	}()

	go func() {
		for i := 0; i < 10000; i++ {
			atomic.AddUint32(&counter, 1) // 原子递增 counter
		}
	}()

	time.Sleep(5 * time.Second) // 等待 Goroutines 完成

	finalCount := atomic.LoadUint32(&counter) // 原子读取 counter 的最终值
	fmt.Println("Final count:", finalCount)   // 输出:Final count: 20000
}

总结来说,sync/atomic 包提供了对整数和指针的原子操作支持,使得在并发环境中对这些数据类型的读取、修改等操作能够保持原子性,避免数据竞争和竞态条件,确保程序的线程安全性。通过使用 sync/atomic 包中的函数,如 Load*Store*Swap*Add*CompareAndSwap* 等,可以在 Go 语言中有效地实现无锁的并发数据结构和算法。