0%

记录几个需要注意的地方

  1. 当一个goroutine尝试发送数据到一个已经关闭的channel时,会引发panic;当一个goroutine尝试从一个已经关闭的channel中接收数据时,会得到一个默认值(zero value)。这个值和channel的声明类型有关系
  2. 数据复制: 在 channel 中传递数据时,数据通常会被复制,这是为了防止数据竞态。这意味着发送方和接收方都拥有它们自己的数据副本;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"fmt"
"unsafe"
)

type Struct1 struct {
a byte
b int32
c byte
}

type Struct2 struct {
a byte
c byte
b int32
}

func main() {
var s1 Struct1
var s2 Struct2
fmt.Printf("Struct1 size: %d, alignment: %d\n", unsafe.Sizeof(s1), unsafe.Alignof(s1))
fmt.Printf("Struct2 size: %d, alignment: %d\n", unsafe.Sizeof(s2), unsafe.Alignof(s2))
}

我们可以看到,当空结构体字段f2在结构体最后一个字段时,是以8字节对齐的,而在St3中,字段f1 的空结构体在第一个字段时,其空间为0。

从上面的例子可以看出,结构体的字段顺序会影响其内存大小和对齐情况。合理安排结构体字段顺序,让占用空间大且对齐要求高的字段尽量靠前,可以减少因对齐而产生的内存空间浪费。

除了空结构体外,空数组也一样。没有任何字段的空 struct{} 和没有任何元素的 数组占据的内存空间大小为 0。因为这一点,空 struct{} 或空数组作为其他结构体 的字段时,一般不需要内存对齐。但是有一种情况除外:即当 struct{} 或空数组作为结构体最后一个字段时,需要内存对齐。

这种机制主要是避免最后一个空 struct{} 或空数组被其他对象引用时, 最后一个字段返回的地址将在结构体之外,如果这个指针引用一直

存活不释放对应的内存,就会有内存泄露的问题。

Go 的结构体字段对齐规则如下:

  1. 结构体字段的对齐值是字段类型的大小和编译器默认的机器字大小中的较小值。例如,在大多数 64 位架构上,机器字大小是 8 字节,那么结构体字段的对齐值将是 8 字节。

  2. 结构体的对齐值是结构体中所有字段对齐值的最大公约数。换句话说,结构体的对齐值是它包含的字段中最大的对齐值。

  3. 结构体的大小是所有字段大小的累加,但会按照对齐值进行填充,以保证结构体的对齐要求。

  4. 如果结构体的对齐值是 1,那么结构体和其字段都不会进行对齐,按照其原始大小进行布局。需要注意的是,结构体的对齐在不同的硬件平台和编译器下的对齐规则可能会有所不同。

  1. 原子操作是指不可中断的操作,要么执行完成,要么不执行,不会出现执行一半的中间状态,就像一个最小的粒子 -原子一样,不可分割;

  2. 原子操作可以理解为变量级别的互斥锁,同一时刻,只能有一个线程或协程对变量进行读或写,所以原子操作具有互斥性;

  3. 在这么多同步原语中真正能够保证原子性执行的只有原子操作(atomic operation)。原子操作在进行的过程中是不允许中断的。在底层是由 CPU 提供芯片级别的支持,即使是在多核CPU或者多个CPU的计算机上,原子操作也是绝对有保障的。原子操作是通过调用LOCK前缀的CPU指令实现的,这些指令,在执行期间不会被其他操作或事件中断并且可以锁定内存总线,防止其他****CPU 访问同一内存地址,从而保证了原子性。

原子操作和锁的区别?

原子操作和锁是两种不同的并发控制机制,它们在处理多线程环境下的数据同步问题时有着不同的作用。

  1. 原子操作:原子操作是指在执行过程中不会被其他线程打断的操作。换句话说,原子操作要么全部执行成功,要么全部不执行。这可以确保数据的一致性和完整性。原子操作通常用于实现计数器、状态变量等简单的数据结构。

  2. 锁:锁是一种同步机制,用于确保多个线程在访问共享资源时不会发生冲突。锁可以保证在同一时刻只有一个线程能够访问共享资源,从而避免数据不一致的问题。

锁分为互斥锁(Mutex)和读写锁(ReadWriteLock)两种类型。互斥锁只允许一个线程独占资源,而读写锁允许多个线程同时读取共享资源,但在写入时会阻塞其他线程。

原子操作和互斥锁的区别?

  1. 实现方式不同。原子操作是通过底层CPU指令实现的,是由 CPU 提供芯片级别的支持。而互斥锁是在软件层面实现的;

  2. 保护范围不同。原子操作保护的对象是单个变量,而互斥锁可以保护一个代码片段;

  3. 性能表现不同。由于原子操作是由底层硬件直接支持的并且保护范围很小,所以性能会更高。

所以在实际使用过程中,如果我们只需要对某一个变量做并发读写,那么使用原子操作就可以了。但是如果我们需要对多个变量做并发读写,那么就需要考虑使用互斥锁了。

信号量的概念

信号量(Semaphore)是一种用于多线程或多进程并发编程中的同步机制,用于协调对共享资源的访问以及控制并发执行的数量。它本质上是一个非负整数变量,并且有两个原子操作(操作过程不可被中断)来对其进行操作,即 P 操作(也常称作 wait 操作)和 S 操作(也常称作 signalpost 操作):

  • P 操作(wait 操作):当一个线程(或进程)执行 P 操作时,会先检查信号量的值,如果信号量的值大于 0,就将信号量的值减 1,表示获取了一个资源或者许可,然后继续执行后续操作;如果信号量的值等于 0,则该线程(或进程)会阻塞,等待信号量的值变为大于 0,直到获取到资源后才能继续执行。
  • S 操作(signalpost 操作):当一个线程(或进程)执行 S 操作时,会将信号量的值加 1,表示释放了一个资源或者许可,如果有其他线程(或进程)正在等待这个信号量(因执行 P 操作而阻塞等待),那么就会唤醒其中的一个线程(或进程),让其可以继续执行。

通过信号量可以实现诸如限制同时访问某个临界区(共享资源所在区域)的线程数量、实现进程间的同步等功能,常见的应用场景包括控制数据库连接池中的连接数量、限制同时访问某个文件的进程数量等。

下面是三方库实现信号量的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"context"
"fmt"
"golang.org/x/sync/semaphore"
"sync"
)

func main() {
// 创建一个最大并发数为3的信号量
s := semaphore.NewWeighted(3)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(num int) {
defer wg.Done()
// 尝试获取信号量许可,设置超时时间(这里超时时间为0,表示不等待直接尝试获取)
err := s.Acquire(context.Background(), 1)
if err!= nil {
fmt.Printf("Goroutine %d 获取许可失败\n", num)
return
}
fmt.Printf("Goroutine %d 获得许可开始执行\n", num)
// 模拟执行任务
defer func() {
fmt.Printf("Goroutine %d 执行完毕,释放许可\n", num)
s.Release(1)
}()
// 这里可以添加具体的任务逻辑,比如访问共享资源等
}(i)
}
wg.Wait()
fmt.Println("所有Goroutine执行完毕")
}