高级
大约 5 分钟
高级
介绍
- 23/2/28
- 理解go某些高级操作。
Select
- select 用于在多线程计算数据
- 下面示例附带了超时机制,但没有default语句.
- 当存在default语句,如果渠道还未存在数据,则直接执行default语句
- 超时机制的作用在于延迟兜底操作。
- default就是渠道都没准备好,就直接执行
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(2 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(3 * time.Second)
c2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
case <-time.After(5 * time.Second):
fmt.Println("timeout") //超时
return
}
}
// 输出
// received one
// received two
}
常见用法
- 1:我们需要一些其他线程的结果聚合就可以使用他,例如1个线程计算从1+至50,另1个线程从51+至100。
- 就可以使用上面的方式进行处理提高效率
- 如下示例
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan int)
c2 := make(chan int)
go func() {
var sum int
for i := 1; i <= 50; i++ {
sum += i
}
c1 <- sum
}()
go func() {
var sum int
for i := 51; i <= 100; i++ {
sum += i
}
c2 <- sum
}()
var sum int
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
sum += msg1
case msg2 := <-c2:
sum += msg2
case <-time.After(1 * time.Second):
//每次只等待1秒超时,如果注释这个case那么就会一直等待,知道渠道发送数据
fmt.Println("timeout")
return
}
}
fmt.Println("结果:", sum)
}
使用注意
- select什么情况下会阻塞?
答
- 当select语句没有任何case语句准备好,也没有default语句时,它就会阻塞,直到至少有一个case语句可以执行1。
- 这意味着select语句会等待所有的channel操作,直到有一个channel可以收到或发送数据2。
- 如果你想避免select语句阻塞,你可以使用default语句或者超时机制。
线程
轻量级线程变量如何处理的?
答
- Go 语言支持并发,可以通过 go 关键字来开启 goroutine,也就是轻量级线程。goroutine 的调度是由 Go 运行时进行管理的。
- 当启动一个 goroutine 时,它会继承当前函数的局部变量的值,并在自己的栈上创建一个副本。如果局部变量是指针或引用类型,
- 那么 goroutine 可能会访问或修改共享的数据。为了避免数据竞争或不一致的问题,
- Go 语言提供了 sync 包中的互斥锁 Mutex 和读写锁 RWMutex 来限制多个 goroutine 对同一个变量的访问3。
- 你可以使用 sync.Mutex 的 Lock 和 Unlock 方法来保护临界区域,
- 或者使用 sync.RWMutex 的 RLock 和 RUnlock 方法来允许多个读操作同时进行,但只允许一个写操作进行。
goroutine和线程区别?
答
- goroutine 是 Go 语言运行时管理的轻量级线程,而线程是操作系统管理的执行单元。
- goroutine 的切换开销比线程小得多,因为 goroutine 的切换只涉及三个寄存器的值修改,而线程的切换涉及模式切换、寄存器刷新等操作。
- goroutine 的栈空间比线程动态灵活,goroutine 一般只需要 2KB 的栈内存,而线程通常需要 2MB。goroutine 的栈大小会根据需要动态地伸缩,而线程的栈大小是固定不变的。
- goroutine 可以轻松创建成千上万个,并发执行,而线程的数量受到操作系统和硬件资源的限制。
goroutine能创建多少个?
答
- goroutine 的数量并不受核心数的限制,而是受到内存和调度器的限制。
- 理论上,goroutine 的数量可以达到几百万个,但实际上这样会导致内存消耗过大和调度开销过高。
- 因此,在实际开发中,需要根据具体的场景和需求来控制 goroutine 的并发数量。
排序
- 可能不经常用到
基本排序
package main
import (
"fmt"
"sort"
)
type StudentTasks []StudentTask
func (s StudentTasks) Len() int {
return len(s)
}
func (s StudentTasks) Less(i, j int) bool {
return s[i].Score > s[j].Score
}
func (s StudentTasks) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
type StudentTask struct {
Score int
}
func main() {
// 创建一个学生切片
students := StudentTasks{
{90},
{60},
{80},
{76},
}
sort.Sort(students)
fmt.Println(students) // [{90} {80} {76} {60}]
}
Chan(缓冲区)
- 多线程单Chan Vs 多线程多Chan
- 由多线程多Chan处理性能更快(都设置了缓冲区)
- 未设置缓存区的Chan 对比 设置缓存区的 Chan 所花费的时间要在2亿循环中,设置缓存区比未设置快大约5倍
package main
import (
"fmt"
"testing"
"time"
)
func TestMultipleWritersToSingleChannel(t *testing.T) {
// 创建一个带有缓冲区的通道
ch := make(chan int, 100000)
// 未设置缓存区 55.9750587s
// 设置缓存区 11.7164381s
// 启动多个协程写入数据
for i := 0; i < 2; i++ {
go func() {
for j := 0; j < 100000000; j++ {
ch <- j
}
}()
}
// 读取所有数据
var count int
start := time.Now()
for range ch {
count++
if count == 200000000 {
break
}
}
elapsed := time.Since(start)
// 检查是否读取了所有数据
fmt.Println("处理数量 -> ", count)
fmt.Printf("处理时间: %s\n", elapsed)
}
func TestMultipleWritersToMultipleChannels(t *testing.T) {
// 创建多个带有缓冲区的通道
ch1 := make(chan int, 100000)
ch2 := make(chan int, 100000)
ch3 := make(chan int, 2)
go func() {
for j := 0; j < 100000000; j++ {
ch1 <- j
}
close(ch1)
}()
go func() {
for j := 0; j < 100000000; j++ {
ch2 <- j
}
close(ch2)
}()
// count 加锁
var count int
go func() {
for {
select {
case _, ok := <-ch1:
if ok {
count++
} else {
ch3 <- 1
}
}
}
}()
go func() {
for {
select {
case _, ok := <-ch2:
if ok {
count++
} else {
ch3 <- 1
}
}
}
}()
start := time.Now()
for i := 0; i < 2; i++ {
select {
case <-ch3:
}
}
fmt.Println("处理数量 -> ", count)
elapsed := time.Since(start)
fmt.Printf("处理时间: %s\n", elapsed)
}
协程上下文
简易版
package main
import (
"bytes"
"context"
"fmt"
"runtime"
"strconv"
"time"
)
// 定义一个上下文,测试是否线程安全
var ctx = NewLocalContext()
func main() {
fmt.Println("main goroutine id:", ctx.GetGID())
Go(1)
Go(2)
time.Sleep(1 * time.Second) // 等待1秒加入上下文
fmt.Println(len(ctx.ctx))
time.Sleep(5 * time.Second)
fmt.Println(len(ctx.ctx))
}
func Go(num int) {
go func() {
for i := 0; i < 3; i++ {
ctx.SaveCtx(num)
// 打印
time.Sleep(time.Second)
// 打印协程id
fmt.Println("上下文数据:", ctx.GetCtx(), "协程id:", ctx.GetGID())
}
ctx.ClearCtx()
}()
}
func (*LocalContext) GetGID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
// SaveCtx 保存上下文
func (l *LocalContext) SaveCtx(data interface{}) {
background := context.Background() // TODO 暂时不可用
background = context.WithValue(background, l.key, data)
l.ctx[l.GetGID()] = background
}
// GetCtx 获取上下文
func (l *LocalContext) GetCtx() interface{} {
return l.ctx[l.GetGID()].Value(l.key) // TODO 暂时不可用
}
// ClearCtx 清理上下文
func (l *LocalContext) ClearCtx() {
delete(l.ctx, l.GetGID())
}
// LocalContext 本地上下文
type LocalContext struct {
ctx map[uint64]context.Context
key string
}
// NewLocalContext 创建本地上下文
func NewLocalContext() *LocalContext {
return &LocalContext{key: "userIdKey", ctx: make(map[uint64]context.Context)}
}