Skip to content

Commit

Permalink
feat: 添加Go语言中原子操作与unsafe.Pointer详解
Browse files Browse the repository at this point in the history
  • Loading branch information
cloaks committed Jan 25, 2025
1 parent 967a6f3 commit 4b2094d
Showing 1 changed file with 139 additions and 0 deletions.
139 changes: 139 additions & 0 deletions docs/blog/posts/2025/1/25-3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
date: 2025-01-25
authors:
- cloaks
categories:
- Golang 编程实践
tags:
- 并发编程
comments: true
---

# 深入理解 Go 的原子操作与 `unsafe.Pointer`

在高并发编程中,原子操作是一种关键技术,用于确保多线程环境下对共享资源的安全操作。本文将深入探讨 Go 中原子操作的原理,为什么需要使用 `unsafe.Pointer`,以及这些操作如何与底层 CPU 的指令配合实现线程安全。

<!-- more -->

## 什么是原子操作?
原子操作指的是不可分割的操作,要么完全执行成功,要么完全不执行,不会被其他线程打断。它是多线程环境中实现线程安全的基础。

在 Go 中,`sync/atomic` 包提供了一系列的原子操作,例如:
- `atomic.LoadPointer`:安全加载指针值。
- `atomic.StorePointer`:安全存储指针值。
- `atomic.CompareAndSwapPointer`:安全地进行比较并交换指针值。

这些操作通过底层硬件支持,能够避免使用锁的额外开销。

## Go 的原子操作如何工作?
### 1. CPU 提供的支持
现代 CPU 提供了一些特殊的指令(如 `LOCK` 前缀的指令),用来保证内存操作的原子性。
- **锁总线**:这些指令会锁住内存总线,确保其他线程无法访问正在操作的内存地址。
- **缓存一致性协议**:通过硬件层的协议(如 MESI 协议),保证多核 CPU 对共享内存的访问一致性。

例如,在 x86 架构上,加载一个指针值可能对应如下汇编指令:
```asm
MOV rax, [addr] ; 从 addr 地址读取指针值到寄存器 rax
```
这是一条原子操作指令,不会被其他线程中断。

### 2. Go 的实现
`atomic.LoadPointer` 为例:
```go
func LoadPointer(addr *unsafe.Pointer) unsafe.Pointer {
return atomic.LoadPointer(addr)
}
```
在运行时,Go 会调用类似于 `MOV``LOCK` 前缀的指令,确保加载操作是不可分割的。

通过这种方式,Go 实现了线程安全的指针操作,避免了竞争条件。

## 为什么原子操作需要 `unsafe.Pointer`
原子操作要求操作的值是固定大小且对齐的,例如 `int32``int64``unsafe.Pointer`。这是因为:
- **CPU 指令限制**:原子操作只能作用于固定大小的内存单元(通常是 1 字节、2 字节、4 字节或 8 字节)。
- **类型兼容性**`unsafe.Pointer` 是 Go 中的通用指针类型,允许对任意指针类型进行操作。

如果直接使用泛型类型(如 `*T`),Go 编译器无法保证类型的大小和对齐方式符合硬件要求。因此,`sync/atomic` 中的操作都基于 `unsafe.Pointer` 实现。

## 原子操作如何避免线程安全问题?
假设有多个线程需要共享一个指针:

```go
package main

import (
"sync/atomic"
"unsafe"
)

var ptr unsafe.Pointer

func main() {
// 线程 1:存储一个新指针
go func() {
newValue := new(int)
*newValue = 42
atomic.StorePointer(&ptr, unsafe.Pointer(newValue))
}()

// 线程 2:加载指针
go func() {
value := atomic.LoadPointer(&ptr)
if value != nil {
println(*(*int)(value)) // 安全读取
}
}()
}
```

### 分析
1. **原子性**`StorePointer``LoadPointer` 确保存储和加载操作不会被中断。
2. **线程安全**:无论线程之间的执行顺序如何,加载到的指针值总是完整的。

## 示例:自定义原子指针类型
以下代码展示了如何使用 `unsafe.Pointer` 实现一个线程安全的指针封装:

```go
package atomicx

import (
"sync/atomic"
"unsafe"
)

// Pointer 是一个泛型原子指针类型。
type Pointer[T any] struct {
v unsafe.Pointer
}

// Load 原子地加载并返回存储的值。
func (x *Pointer[T]) Load() *T {
return (*T)(atomic.LoadPointer(&x.v))
}

// Store 原子地存储一个新值。
func (x *Pointer[T]) Store(val *T) {
atomic.StorePointer(&x.v, unsafe.Pointer(val))
}

// Swap 原子地存储新值并返回旧值。
func (x *Pointer[T]) Swap(new *T) (old *T) {
return (*T)(atomic.SwapPointer(&x.v, unsafe.Pointer(new)))
}

// CompareAndSwap 执行原子比较并交换操作。
func (x *Pointer[T]) CompareAndSwap(old, new *T) bool {
return atomic.CompareAndSwapPointer(&x.v, unsafe.Pointer(old), unsafe.Pointer(new))
}
```

### 特点
- **线程安全**:所有操作都是原子的。
- **高性能**:避免了锁的使用。

## 总结
1. **原子操作的重要性**:在多线程环境中,原子操作提供了一种高效的方式来实现线程安全。
2. **为什么使用 `unsafe.Pointer`**:它是 Go 中唯一通用的指针类型,可以兼容底层硬件的要求。
3. **与 CPU 的协作**:Go 的原子操作直接调用了底层硬件的原子指令,避免了锁的开销,最大化了性能。

通过 `sync/atomic``unsafe.Pointer`,我们可以在 Go 中实现高效且线程安全的操作,是多线程编程中非常重要的工具。

0 comments on commit 4b2094d

Please sign in to comment.