Go面试题(二)
1、怎么做代码优化
减少内存分配
内存分配是任何程序的基本操作之一,也是一个明显的性能瓶颈。在Golang中,减少内存分配是一种有效的代码优化方式。为了减少内存分配,我们可以使用以下技巧:
-
复用变量:在循环或迭代过程中,尽量避免重新分配变量。通过在循环外部定义和初始化变量,并在迭代中重复使用,可以降低内存分配的频率。
-
使用零值初始化:在声明变量时,可以利用Golang的零值特性初始化变量。这样可以避免不必要的内存分配。例如,对于整数类型,未显式赋值的变量默认为零值0。
-
使用切片:切片是Golang中一种动态数组的数据结构。与传统的数组相比,切片具有更高的灵活性和效率。通过使用切片,可以减少内存的分配和复制操作。
减少系统调用
系统调用是操作系统提供给应用程序的接口,用于执行诸如文件读写、网络通信等操作。在Golang中,减少系统调用可以提高代码的性能和效率。以下是一些减少系统调用的方法:
-
批量操作:对于需要执行多个系统调用的操作,可以尝试将其合并为一个批量操作。这样可以减少系统调用的次数,从而提高代码的性能。
-
使用缓存:对于频繁访问的数据,可以使用缓存来减少系统调用。缓存可以将数据保存在内存中,避免了从磁盘或网络中读取的开销。
-
异步IO:通过使用Golang提供的异步IO机制,可以实现非阻塞的系统调用。这样可以减少CPU的等待时间,提高代码的并发性。
并发编程
并发是Golang的一大特点,也是其在处理大规模数据和高并发场景下具有优势的重要原因。在进行代码优化时,我们可以利用Golang提供的丰富的并发编程工具来提高代码的性能:
-
Goroutine和Channel:通过使用Goroutine和Channel,我们可以将任务分解为多个独立的执行单元,并利用Channel进行数据传输和同步。这样可以实现高效的并发处理。
-
使用互斥锁:在多个Goroutine并发访问共享资源时,为了保证数据的一致性,我们可以使用互斥锁(Mutex)来控制对共享资源的访问。互斥锁可以保证同一时间只有一个Goroutine可以访问共享资源。
-
使用WaitGroup:在多个Goroutine并发执行任务时,我们可以使用WaitGroup来等待所有任务完成。WaitGroup可以协调Goroutine的执行顺序,并确保所有任务都已完成。
注意:
过早的优化是万恶之源,千万不要为了优化而优化:
- pprof分析,竞态分析,逃逸分析,这些基础的手段是必须要学会的。
- 常规的优化技巧是比较实用的,他们往往能解决大部分的性能问题并且足够安全。
- 在一些着重性能的基础库中,使用一些非常规的优化手段也是可以的,但必须要权衡利弊,不要过早放弃可读性,兼容性和稳定性。
2、如何访问私有成员
在 Go 语言中,以小写字母开头的标识符是私有成员,私有成员(字段、方法、函数等)遵循语言的可见性规则,仅在定义它的包内可见,包外无法访问这些私有成员。如果想要访问私有成员,主要包括以下三种方式:
在同一个包内,可以直接访问小写字母开头的私有成员。.
在其他包中,无法直接访问私有成员,但可以通过公开的接口来间接访问私有成员。
使用反射来绕过 Go 语言的封装机制访问和修改私有字段。(不建议使用)。
扩展知识
访问私有成员的规则
可见性规则:
私有成员:以小写字母开头的标识符是私有的,仅在定义它的包内可见。包外无法访问这些私有成员。
公开成员:以大写字母开头的标识符是公开的,可以在任何包中访问。
3、反射应用场景
反射机制在 Golang 中是通过 reflect 包来实现的,reflect 包提供了两个主要的类型:reflect.Type 和 reflect.Value。
使用场景:
-
动态类型转换:通过反射可以实现不同类型之间的动态转换。
-
JSON 序列化和反序列化:许多 JSON 库如 encoding/json 就大量使用了反射。
-
ORM 框架:数据库 ORM 框架如 Gorm、Xorm 等也依赖反射来处理数据库记录和 Go 对象之间的转换。
-
动态代理和 AOP 编程:反射可以用于实现动态代理和面向切面编程。
-
测试和 Mocking:在单元测试中,反射可以用来访问和设置私有成员变量,或者调用私有方法,以便于测试内部状态或行为。
反射的性能考量
反射的操作通常比直接操作性能要差,主要体现在:
-
类型检查:反射需要在运行时检查变量的类型信息,这是一个动态过程,无法在编译时优化。
-
动态调用:使用反射调用方法时,不能像普通方法调用那样直接编译到具体的机器代码上,而是需要通过反射的方式查找到方法,并且在运行时进行调用。这个查找和动态调用的过程比直接调用方法要慢得多。
-
内存分配:在使用反射时,经常需要进行额外的内存分配。例如,当使用 reflect.ValueOf() 函数时,会创建一个新的 reflect.Value 类型的实例,这个实例包含了原始值的副本以及类型信息。这些额外的内存分配和后续的垃圾回收都会影响性能。
-
逃逸分析:在使用反射时,很多变量可能会被认为是“逃逸”到函数外部,即使实际上并没有。会导致这些变量被分配到堆上,而不是栈上,增加了垃圾回收的压力。
-
接口包装:反射操作通常涉及到将具体的值包装到 interface{} 类型中,需要运行时的类型信息,这个包装过程也是有性能开销的。
-
代码复杂性:使用反射的代码往往比直接的代码要复杂,可能会导致编译器难以进行针对性的优化。
反射的最佳实践
-
避免不必要的反射:只有在需要处理未知类型的数据,或者需要创建非常通用的函数时,才应该使用反射。
-
缓存反射结果:如果需要对同一个类型进行多次反射操作,考虑缓存 Type 和 Value 对象以提高性能。
-
使用类型断言和类型切换:当可以确定值的类型范围时,使用类型断言和类型切换通常比使用反射更清晰和高效。
-
理解可设置性(settability):在尝试修改值之前,始终检查值是否可设置。
-
处理错误:当使用反射 API 时,代码更容易出错,因为在编译时不能进行类型安全检测。务必检查错误,例如调用 CanSet、CanInterface 等方法时,并处理这些情况。
-
安全性:反射可以绕过一些类型检查和限制,允许开发者执行一些平常不被允许的操作,如访问私有字段,会破坏对象的封装性和数据的完整性。
-
可读性和可维护性:反射代码的逻辑往往不如静态类型代码直观,且错误在运行时才会暴露,更难理解和维护。
4、Go的IO介绍
常规读写
os.Mkdir(name string, perm FileMode) error // 仅创建一层
os.MkdirAll(path string, perm FileMode) error // 创建多层
os.Create(name string) (file *File, err error) // 存在则覆盖
os.Open(name string) (file *File, err error) // 只读方式打开文件
os.OpenFile(name string, flag int, perm FileMode) (file *File, err error) // parm控制权限,例如0066、0777
file.Close() error // 关闭文件,断开程序与文件的连接
os.Remove(name string) error // 删除文件一层
os.RemoveAll(path string) error // 级联删除
带缓冲读写bufio
io.Reader和io.Writer
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
5、引用类型和值类型及内存分配
值类型:基本数据类型int、float、bool、string以及数组和struct
引用类型:指针、slice、map、chan、接口、函数等都是引用类型
全局变量
- 引用类型的分配在堆上,值类型的分配在栈上。
- 一般分配在栈上。如果局部变量太大,则分配在堆上。如果函数执行完,仍然有外部引用此局部变量,则分配在堆上。
6、slice扩容规则
规则1:需要增长到的容量cap是原始容量的两倍还多,则扩容到cap;
规则2:需要增长的容量小于原容量2倍,但是原用量小于1024,则2倍扩容;
需要增长的容量小于原容量2倍,但是原容量大于1024,则1.25倍扩容;
扩容后需要分配的内存,并不是扩容后的容量乘以数据类型字节。而是根据扩容后的容量去内存管理模块申请最匹配且覆盖的容量,根据这个容量乘以数据类型,才是最终需要分配的内存空间
7、切片Slice的长度len与容量cap
切片拥有长度和容量。
切片的长度是它所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)
// 截取切片使其长度为 0
s = s[:0]
printSlice(s)
// 拓展其长度
s = s[:4]
printSlice(s)
// 舍弃前两个值
s = s[2:]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
/*
len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]
*/
长度是切片引用的元素数目。容量是底层数组的元素数目(从切片指针开始)
8、有缓冲channel和无缓冲channel
ch := make(chan int) 无缓冲的channel由于没有缓冲发送和接收需要同步.
ch := make(chan int, 2) 有缓冲channel不要求发送和接收操作同步.
channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。
channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。
9、总结一下操作 channel 的结果
操作 | nil channel | closed channel | not nil, not closed channel |
---|---|---|---|
close | panic | panic | 正常关闭 |
读 <- ch | 阻塞 | 读到对应类型的零值 | 阻塞或正常读取数据。缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞 |
写 ch <- | 阻塞 | panic | 阻塞或正常写入数据。非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞 |
总结
发生 panic 的情况有三种:向一个关闭的 channel 进行写操作;关闭一个 nil 的 channel;重复关闭一个 channel。
读、写一个 nil channel 都会被阻塞。
10、说说对Context的理解
概念:
Context是由Golang官方开发的并发控制包,一方面可以用于当请求超时或者取消时候,相关的goroutine马上退出释放资源,另一方面Context本身含义就是上下文,其可以在多个goroutine或者多个处理函数之间传递共享的信息。
创建一个新的context,必须基于一个父context,新的context又可以作为其他context的父context。所有context在一起构造一个context树。
Context用途:
- Context一大用处就是超时控制。
- Context另外一个用途就是传递上下文信息。
Context接口一共包含四个方法:
- Deadline:返回绑定该context任务的执行超时时间,若未设置,则ok等于false
- Done:返回一个只读通道,当绑定该context的任务执行完成并调用cancel方法或者任务执行超时时候,该通道会被关闭
- Err:返回一个错误,如果Done返回的通道未关闭则返回nil,如果context如果被取消,返回Canceled错误,如果超时则会返回DeadlineExceeded错误
- Value:根据key返回,存储在context中k-v数据
使用Context注意事项:
- 不要将Context作为结构体的一个字段存储,相反而应该显示传递Context给每一个需要它的函数,Context应该作为函数的第一个参数,并命名为ctx
- 不要传递一个nil Context给一个函数,即使该函数能够接受它。如果你不确定使用哪一个Context,那你就传递context.TODO
- context是并发安全的,相同的Context能够传递给运行在不同goroutine的函数
11、谈谈对defer关键字的了解
- 知识点1: defer的执行顺序
多个defer出现的时候,它是一个“栈”的关系,也就是先进后出。一个函数中,写在前面的defer会比写在后面的defer调用的晚。
- 知识点2:defer与return谁先谁后
return之后的语句先执行,defer后的语句后执行
- 知识点3:函数的返回值初始化与defer间接影响
只要声明函数的返回值变量名称,就会在函数初始化时候为之赋值为0,而且在函数体作用域可见。
- 知识点4:有名函数返回值遇见defer情况
在没有defer的情况下,其实函数的返回就是与return一致的,但是有了defer就不一样了。
我们通过知识点2得知,先return,再defer,所以在执行完return之后,还要再执行defer里的语句,依然可以修改本应该返回的结果。
func returnButDefer() (t int) { //t初始化0, 并且作用域为该函数全域
defer func() {
t = t * 10
}()
return 1
}
func main() {
fmt.Println(returnButDefer())
}
/*
该returnButDefer()本应的返回值是1,但是在return之后,
又被defer的匿名func函数执行,所以t=t*10被执行,
最后returnButDefer()返回给上层main()的结果为10
*/
- 知识点5:defer遇见panic
遇到panic时,遍历本协程的defer链表,并执行defer。在执行defer过程中:遇到recover则停止panic,返回recover处继续往下执行。如果没有遇到recover,遍历完本协程的defer链表后,向stderr抛出panic信息。
func main() {
defer_call()
fmt.Println("main 正常结束")
}
func defer_call() {
defer func() { fmt.Println("defer: panic 之前1") }()
defer func() { fmt.Println("defer: panic 之前2") }()
panic("异常内容") //触发defer出栈
defer func() { fmt.Println("defer: panic 之后,永远执行不到") }() //在panic之后,并没有扫描和执行到
}
/*
defer: panic 之前2
defer: panic 之前1
panic: 异常内容
... 异常堆栈信息
*/
defer 最大的功能是 panic 后依然有效
所以defer可以保证你的一些资源一定会被关闭,从而避免一些异常出现的问题。
- 知识点6:defer中包含panic
func main() {
defer func() {
if err := recover(); err != nil{
fmt.Println(err)
}else {
fmt.Println("fatal")
}
}()
defer func() {
panic("defer panic")
}()
panic("panic")
}
/*
输出:defer panic
*/
panic仅有最后一个可以被revover捕获。
触发panic("panic")
后defer顺序出栈执行,第一个被执行的defer中 会有panic("defer panic")
异常语句,这个异常将会覆盖掉main中的异常panic("panic")
,最后这个异常被第二个执行的defer捕获到。
- 知识点7:defer下的函数参数包含子函数
func function(index int, value int) int {
fmt.Println(index)
return index
}
func main() {
defer function(1, function(3, 0))
defer function(2, function(4, 0))
}
4个函数,他们的index序号分别为1,2,3,4执行顺序如下:
- defer压栈function1,压栈函数地址、形参1、形参2(调用function3) --> 打印3
- defer压栈function2,压栈函数地址、形参1、形参2(调用function4) --> 打印4
- defer出栈function2, 调用function2 --> 打印2
- defer出栈function1, 调用function1--> 打印1
12、断言应用场景
Golang 中的接口是一种抽象类型,可以存储任何实现了该接口方法的类型实例。然而,由于接口本身不包含类型信息,需要通过类型断言来将接口变量转换为实际类型。
类型断言的基本语法如下:
value, ok := x.(T)
x 是一个接口类型的变量,T 是希望断言的类型。value 将会是 x 转换为类型 T 后的值,ok 是一个布尔值,当类型断言成功时为 true,失败时为 false
类型断言主要用于以下几种场景:
- 检查类型:确定接口值的具体类型。
- 接口值的类型转换:将接口值转换为具体的类型。
- 实现多态行为:Golang 中的多态主要是通过接口实现的,根据接口值的具体类型执行不同的操作,从而实现多态。
类型断言的最佳实践
-
避免过度依赖类型断言,频繁使用类型断言可能是设计上的问题。如果发现自己在使用大量的类型断言的时候,需要停下来审视下类型设计是否合理,良好的设计应尽量减少类型断言的使用。
-
安全地使用类型断言,尽可能使用带 ok 的形式进行类型断言,避免程序 panic,使程序更加健壮。
-
当有多个可能的类型需要断言时,可以使用类型分支(type switch),这是一种特殊的类型断言形式,可以更清晰地处理多个类型。
13、init函数
概念:
init函数先于main函数执行,实现包级别的一些初始化操作
一个包中,可以包含多个 init 函数;
程序编译时,先执行依赖包的 init 函数,再执行 main 包内的 init 函数;可以用_来忽略导入包,但是执行该包的init函数
main包也可以包含不止一个Init函数,且init函数不能被其他函数显示调用,否则编译错误
主要作用:
- 初始化不能采用初始化表达式初始化的变量。
- 程序运行前的注册。
- 实现sync.Once功能。
主要特点:
- init函数先于main函数自动执行,不能被其他函数调用;
- init函数没有输入参数、返回值;
- 每个包可以有多个init函数;
- 包的每个源文件也可以有多个init函数;
- 同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序(以上述程序输出来看,执行顺序是源文件名称的字典序)。
- 不同包的init函数按照包导入的依赖关系决定执行顺序。
14、接口的特点和用法
概念:
接口定义了一组方法的集合,它描述了类型应该具备的能力。Go 的接口只关心方法的签名,不关心方法的具体实现。
核心思想:只要一个类型实现了接口中的所有方法,该类型就被认为实现了该接口(即隐式实现)。
优势:接口的使用使得代码更加模块化,易于维护和扩展,实现了“面向接口编程”这种解耦的编程方式。
接口的语法
type 接口名 interface {
方法1(参数列表) 返回值类型
方法2(参数列表) 返回值类型
}
实现接口
Go 语言中的接口是隐式实现的,也就是说,如果一个类型实现了一个接口定义的所有方法,那么它就自动地实现了该接口。因此,我们可以通过将接口作为参数来实现对不同类型的调用,从而实现多态。
需要注意的是,接口类型变量可以存储任何实现了该接口的类型的值。
15、go语言关键字
下面列举了 Go 代码中会使用到的 25 个关键字或保留字:
break | default | func | interface | select |
case | defer | go | map | struct |
chan | else | goto | package | switch |
const | fallthrough | if | range | type |
continue | for | import | return | var |
除了以上介绍的这些关键字,Go 语言还有 36 个预定义标识符:
append | bool | byte | cap | close | complex | complex64 | complex128 | uint16 |
copy | false | float32 | float64 | imag | int | int8 | int16 | uint32 |
int32 | int64 | iota | len | make | new | nil | panic | uint64 |
println | real | recover | string | true | uint | uint8 | uintptr |
程序一般由关键字、常量、变量、运算符、类型和函数组成。
程序中可能会使用到这些分隔符:括号 (),中括号 [] 和大括号 {}。
程序中可能会使用到这些标点符号:.、,、;、: 和 …。
16、数据类型
在 Go 编程语言中,数据类型用于声明函数和变量。
数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。
Go 语言按类别有以下几种数据类型:
Go 语言按类别有以下几种数据类型:
序号 | 类型和描述 |
---|---|
1 | 布尔型 布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。 |
2 | 数字类型 整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。 |
3 | 字符串类型: 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。 |
4 | 派生类型: 包括:
|
17、变量作用域
作用域为已声明标识符所表示的常量、类型、变量、函数或包在源代码中的作用范围。
Go 语言中变量可以在三个地方声明:
- 函数内定义的变量称为局部变量
- 函数外定义的变量称为全局变量
- 函数定义中的变量称为形式参数
局部变量
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。
全局变量
在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。
形式参数
形式参数会作为函数的局部变量来使用。
18、在Go语言中,如何实现并发同步
Go语言提供了多种并发同步机制,包括互斥锁(sync.Mutex)、等待组(sync.WaitGroup)、条件变量(sync.Cond)和通道(channel)。互斥锁用于保护并发访问共享资源,等待组用于等待一组goroutines完成,条件变量用于在满足特定条件时通知等待的goroutines,而通道则用于在goroutines之间传递消息和同步执行。
19、解释Go语言中接口的动态性如何工作
Go语言的接口是动态的,这意味着在运行时才能确定接口变量实际指向的具体类型。这允许接口变量在不同的上下文中持有不同类型的值,而不需要在编译时确定。动态性使得接口在编写通用代码和处理多种类型时非常有用。
20、Go提供了哪些并发原语
Go提供了多种并发原语,包括goroutines、channels、互斥锁(sync.Mutex)、等待组(sync.WaitGroup)、条件变量(sync.Cond)、原子操作(sync/atomic)和一次性锁(sync.Once)等。这些原语使得并发编程在Go中变得简单而高效。
21、Mutex的两种模式
Mutex 可能处于两种操作模式下:正常模式和饥饿模式
正常模式
在正常模式下,所有的goroutine会按照先进先出的顺序进行等待,被唤醒的goroutine不会直接持有锁,会和新进来的锁进行竞争,新请求进来的锁会更容易抢占到锁,因为正在CPU上运行,因此刚唤醒的goroutine可能会竞争失败,回到队列头部;如果队列的goroutine超过1毫秒的等待时间,则会转换到饥饿模式。
饥饿模式
在饥饿模式下,锁会直接交给队列的第一个goroutine,新进来的goroutine不会抢占锁也不会进入自旋状态,直接进入队列尾部;如果当前goroutine已经是队列的最后一个或者当前goroutine等待时间小于1毫秒,则会转换到正常模式
正常模式下,性能更好,但饥饿模式解决取锁公平问题,性能较差。
22、死锁
概念
两个或两个以上的进程(或线程,goroutine)在执行过程中,因争夺共享资源而处于一种互相等待的状态,如果没有外部干涉,它们都将无法推进下去,此时,我们称系统处于死锁状态或系统产生了死锁。
产生死锁的四个必要条件
-
互斥:资源只能被一个goroutine持有,其他gouroutine必须等待,直到资源被释放
-
持有和等待:goroutine 持有一个资源,并且还在请求其它 goroutine 持有的资源
-
不可剥夺:资源只能由持有它的 goroutine 来释放
-
环路等待:多个等待goroutine(g1,g2,g3),g1等待g2的资源,g2等待g3的资源,g3等待g1的资源,形成环路等待的死结
如何解决死锁(只需要打破必要条件其中一个即可避免死锁)
-
设置超时时间
-
避免使用多个锁
-
按照规定顺序申请锁
-
死锁检测
23、sync.Cond
Cond 通常应用于等待某个条件的一组 goroutine,等条件变为 true 的时候,其中一个 goroutine 或者所有的 goroutine 都会被唤醒执行。
基本方法
func NeWCond(l Locker) *Cond
func (c *Cond) Broadcast()
func (c *Cond) Signal()
func (c *Cond) Wait()
- Singal(): 唤醒一个等待此 Cond 的 goroutine
- Broadcast(): 唤醒所有等待此 Cond 的 goroutine
- Wait(): 放入 Cond 的等待队列中并阻塞,直到被 Signal 或者 Broadcast 的方法从等待队列中移除并唤醒,使用该方法是需要搭配满足条件
24、SingleFlight
基本概念
SingleFlight 是 Go 开发组提供的一个扩展并发原语。它的作用是,在处理多个 goroutine 同时调用同一个函数的时候,只让一个 goroutine 去调用这个函数,等到这个 goroutine 返回结果的时候,再把结果返回给这几个同时调用的 goroutine,这样可以减少并发调用的数量。
与sync.Once的区别
-
sync.Once 不是只在并发的时候保证只有一个 goroutine 执行函数 f,而是会保证永远只执行一次,而 SingleFlight 是每次调用都重新执行,并且在多个请求同时调用的时候只有一个执行。
-
sync.Once 主要是用在单次初始化场景中,而 SingleFlight 主要用在合并并发请求的场景中
应用场景
使用 SingleFlight 时,可以通过合并请求的方式降低对下游服务的并发压力,从而提高系统的性能,常常用于缓存系统中。
25、解释一下并发编程中的自旋状态
自旋状态是并发编程中的一种状态,指的是线程或进程在等待某个条件满足时,不会进入休眠或阻塞状态,而是通过不断地检查条件是否满足来进行忙等待。
在自旋状态下,线程会反复执行一个忙等待的循环,直到条件满足或达到一定的等待时间。 这种方式可以减少线程切换的开销,提高并发性能。然而,自旋状态也可能导致CPU资源的浪费,因为线程会持续占用CPU时间片,即使条件尚未满足。
type SpinLock uint32
// Lock 尝试获取锁,如果锁已经被持有,则会自旋等待直到锁释放
func (sl *SpinLock) Lock() {
for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
runtime.Gosched() // 不要占满整个CPU,让出时间片
}
}
// Unlock 释放锁
func (sl *SpinLock) Unlock() {
atomic.StoreUint32((*uint32)(sl), 0)
}
// NewSpinLock 创建一个自旋锁
func NewSpinLock() *SpinLock {
return new(SpinLock)
}
func main() {
lock := NewSpinLock()
lock.Lock()
// 临界区
time.Sleep(1 * time.Second) // 模拟临界区操作
lock.Unlock()
}
自旋状态通常用于以下情况:
- 在多处理器系统中,等待某个共享资源的释放,以避免线程切换的开销。
- 在短暂的等待时间内,期望条件能够快速满足,从而避免进入阻塞状态的开销。
自旋锁与互斥锁的选择
在决定使用自旋锁还是互斥锁时,需要考虑以下因素:
- 锁的持有时间:如果锁的持有时间非常短,自旋锁可能更合适。
- 锁的竞争程度:如果锁的竞争比较小,自旋锁可能更高效。
- CPU 核心数量:在多核处理器上,自旋锁可以在一个核上自旋,而不会影响到其他核心。
需要注意的是,自旋状态的使用应该谨慎,并且需要根据具体的场景和条件进行评估。如果自旋时间过长或条件不太可能很快满足,那么使用自旋状态可能会浪费大量的CPU资源。在这种情况下,更适合使用阻塞或休眠等待的方式。
总之,自旋状态是一种在等待条件满足时不进入休眠或阻塞状态的并发编程技术。它可以减少线程切换的开销,但需要权衡CPU资源的使用和等待时间的长短。
标签:defer,day09,函数,goroutine,笔记,Golang,Go,类型,panic From: https://blog.csdn.net/Runing_WoNiu/article/details/142971164